目录
一、创建线程
1.1.第一种方法:继承Thread类
1.2.第二种方法:实现Runnable接口
1.3.其他创建线程的方法
二、多线程的优势-增加运行速度
三、Thread类及常见方法
3.1 Thread常见的构造方法
3.2Thread的几个常见方法
3.2.1启动一个线程——start()
3.2.2中断一个线程
3.2.3 等待一个线程——join()
3.2.4 获取当前线程引用
3.2.5 休眠当前线程
四、线程的状态
4.1 观察线程的所有状态
4.2线程状态和状态转移的意义
五、多线程带来的风险——线程安全(重点)
5.1 观察线程不安全
5.2 线程安全的概念
5.3 线程不安全的原因
(1)修改共享数据
(2)可见性
(3)可见性和原子性
(4)代码顺序性
5.4 解决上面的线程不安全问题
六、synchronized关键字-监视器锁monitor lock
6.1 synchronized的特性
1)互斥
2)刷新内存
3)可重入
6.2 synchronized使用示例
6.3 关于死锁
七、volatile关键字(保证内存可见性)
八.wait和notify
8.1 wait()方法
8.2 notify()方法
8.3 notifyAll()方法
8.4 wait()和sleep的对比
8.5 线程饿死
线程是操作系统的概念,操作系统提供了一些API(应用程序编程接口),可以操作线程,Java针对上述系统API进行了封装,我们只需要掌握这一套API就可以了。
一、创建线程
1.1.第一种方法:继承Thread类
1.继承Thread来创建一个线程类
Thread是标准库内置的类,是在java.lang这个包下的,不需要import
class MyThread extends Thread{
@override
public void run(){
System.out.println("这里是线程运行的代码");
}
}
说明:此处的run()方法是重写了Thread类里面的方法,而Thread里面的run()方法是实现了Runnable接口的run()方法,后面的内容会讲到。
2.创建MyThread类实例
Thread t = new MyThread();
3.调用start方法
t.start(); //线程开始运行
说明:start后面会讲
完整代码:
class MyThread extends Thread{
@Override
public void run() {
System.out.println("MyThread");
}
}
public class demo1 {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
1.2.第二种方法:实现Runnable接口
1.实现Runnable接口
class MyRunnable implements Runnable{
@override
public void run(){
System.out.println("这里是线程运行的代码");
}
}
说明:Runnable是一个接口,通过像Thread一样实现Runnable接口,补充函数体。
2.创建Thread类实例,调用Thread的构造方法时将Runnable对象作为target参数。
Thread t = new Thread(new MyRunnable());
Thread构造方法源码(这只是其中一个构造方法):
3.调用start方法
t.start(); //线程开始执行
说明:start后面会讲
完整代码:
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable");
}
}
public class demo1 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
对比上面两种方法:
- 继承Thread类,直接使用this就表示当前线程对象的引用。
- 实现Runnable接口,this表示的是MyRunnable的引用,要引用当前线程,需要使用Thread.curremtThread()。
1.3.其他创建线程的方法
- 匿名内部类创建Thread子类对象
//使用匿名类创建Thread子类对象
Thread t1 = new Thread(){
@override
public void run(){
System.out.println("使用匿名类创建Thread子类对象");
}
};
- 匿名内部类创建Runnable子类对象
//使用匿名类创建Runnable子类对象
Thread t2 = new Thread(new Runnable(){
@override
public void run(){
System.out.println("使用匿名类创建Runnable子类对象");
}
});
上面的例子分别创建了继承Thread类且重写run()方法的匿名内部类、创建了实现Runnable接口的匿名内部类。
- lambda表达式创建Runnable子类对象
//1使用lambda表达式创建Runnable子类对象
Thread t3 = new Thread(()->
System.out.println("使用匿名类创建Thread子类对象"));
Thread t4 = new Thread(()->{
System.out.println("使用匿名类创建Thread子类对象")
});
说明:lambda表达式(匿名函数)的使用要满足函数式接口,其是用来实现接口中的函数,创建了一个可调用的函数对象。Thread中放的是一般是Runnable接口类型的实例,当换成lambda表达式时,Java编译器会推断出该是Runnable接口的函数且实现的是Runnable接口的函数。
(lambda表达式赋值给哪个函数式接口类型的变量,编译器就知道这个变量的类型,从而确定需要实现哪个方法)。
比如这样:
Runnable runnable = ()->System.out.println("使用匿名类创建Thread子类对象");
Thread t = new Thread(runnable);
二、多线程的优势-增加运行速度
每个线程都是一个独立的执行流,每个线程都可以执行一系列的逻辑代码。
可以观察多线程在一些场合下是可以提高程序的整体运行效率的。
- 使用System.nanoTime()可以记录当前系统的纳秒级时间戳。
- serial串行的完成一系列运算,concurrency使用两个线程并行的完成同样的运算。
通过设置不同的count来比较并发和串行的执行速度:
public class ThreadAdvantage {
//多线程并不一定就能提高速度,可以观察,count不同,实际的运行效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
//使用并发方式
concurrency();
//使用串行方式
serial();
}
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
//利用一个线程计算a的值
Thread thread = new Thread(()->{
int a = 0;
for (long i =0;i<count;i++){
a--;
}
});
thread.start();
//主线程内计算b放入值
int b = 0;
for(long i =0 ;i<count;i++){
b--;
}
//该函数线程等待thread线程运行结束
thread.join();
//统计耗时
long end = System.nanoTime();
double ms =(end- begin)*1.0/1000/1000;
System.out.printf("并发:%f 毫秒%n",ms);
}
private static void serial(){
//全部在主线程内计算a、b的值
long begin = System.nanoTime();
int a= 0;
for (long i =0;i<count;i++){
a--;
}
int b = 0;
for (long i =0 ;i<count;i++){
b--;
}
long end = System.nanoTime();
double ms = (end -begin)*1.0/1000/1000;
System.out.printf("串行:%f 毫秒%n",ms);
}
}
count越大并发执行的效率越快。反而越小,并发执行的效率反而不如串行执行的效率,这样的情况系并发执行往往是在线程创建和管理等的消耗上。
三、Thread类及常见方法
Thread类是jvm用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread对象与之关联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图,而Thread类的对象就是用来描述一个线程执行流,jvm会将这些Thread对象组织起来,用于线程调度,线程管理。
3.1 Thread常见的构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象并命名 |
【了解】Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这个本篇了解即可 |
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(),"这是我的名字");
3.2Thread的几个常见方法
属性 | 获取方法 | 返回值 | 作用 |
ID | getId() | long | 线程的身份标识,标识一个进程中唯一的一个线程 |
名称 | getName() | String | 获取线程的名称(各种调试工具会用到) |
状态 | getState() | State枚举类 | 获取线程的状态(后面会说明) |
优先级 | getPriority() | int | 获取线程的优先级(理论上来说优先级高的更容易被调度到) |
是否是后台线程 | isDaemon() | boolean | 判断是否为后台线程(守护线程) |
是否存活 | isAlive() | boolean | 判断线程是否已经启动或尚未终止 |
是否被中断 | isInterrupted() | boolean | 判断是否处于中断状态(后面会进一步说明) |
- 关于后台线程(守护线程)和前台线程:
前台线程结束,程序结束,后台线程也会强制终止,而JVM的终止依赖于前台线程。
后台线程不结束不影响整个线程的进程的结束。
JVM中有进程,单个进程有多个线程,线程被分为前台、后台线程。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for(int i =0;i<10;i++){
try{
System.out.println(Thread.currentThread().getName()+":我还活着");
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+": 我即将死去");
});
System.out.println(thread.getName()+":ID:"+thread.getId());
System.out.println(thread.getName()+":名称:"+thread.getName());
System.out.println(thread.getName()+":状态:"+thread.getState());
System.out.println(thread.getName()+":优先级:"+thread.getPriority());
System.out.println(thread.getName()+":后台线程:"+thread.isDaemon());
System.out.println(thread.getName()+":活着:"+thread.isAlive());
System.out.println(thread.getName()+"被中断:"+thread.isInterrupted());
thread.start();
while(thread.isAlive()){
}
System.out.println(thread.getName()+":状态:"+thread.getState());
}
}
3.2.1启动一个线程——start()
之前通过上面看到了如何重写run方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
start方法内部,是会调用到系统的api赖在系统内核中创建出线程。
run方法就只是单纯的描述了该线程要执行啥内容(会在start创建好线程之后自动被调用的)。
- 重写run方法是提供给线程要做的事情的指令清单。
- 现成对象可以认为是把李四、王五叫过来了。
- 而调用start()方法,就是喊一声“行动起来!”,现成才真正独立去执行了。
调用start方法,才真正在操作系统的底层创建出一个线程。
3.2.2中断一个线程
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到我们的停止线程的方式了。
目前常见的有以下两种方式:
- 通过共享的标记来进行沟通
- 调用interrupt()方法来通知
- 示例1:使用自定义的变量来作为标志位
public class TreadDemo {
private static class MyRunnable implements Runnable{
public static boolean isQuit = false;
public void run(){
while (!isQuit){
System.out.println(Thread.currentThread().getName()+
":别管我,我在忙着转账呢!");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":啊!差点误了大事!");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target,"李四");
System.out.println(Thread.currentThread().getName()
+":让李四开始转账。");
thread.start();
Thread.sleep(10*1000);
System.out.println(Thread.currentThread().getName()
+":老板来电话了,赶紧通知李四对方是个骗子!");
MyRunnable.isQuit = true;
}
}
- 示例2:使用Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位。
Thread内部包含了一个boolean类型的变量作为线程是否被中断的标记,默认标记位为false。被中断,标志位为true,未被中断,标志位为false。
关于中断的方法:
方法 | 说明 |
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知。若线程未阻塞,线程调用该函数时,该线程标志位为true。 |
public static boolean interrupted() | 判断当前线程的中断标志位是为false或true。当前线程调用后标志位会被清除为false。 |
public boolean isinterrupted() | 判断对象关联的线程的标志位是为false或true。调用后 不清除标志位 |
注意:调用interrupt()后并不会终止线程,而是把该线程的标志位由false变为true,然后再配合interrupted()或isinterrupted()来进行对线程的终止。
- 使用thread对象的interrupted()方法或isInterrupted()通知线程结束
public class ThreadDemo {
private static class MyRunnable implements Runnable{
@Override
public void run() {
//两种方法都可以
//while(!Thread.currentThread().isInterrupted()){
while(!Thread.interrupted()){
System.out.println(Thread.currentThread().getName()
+":别管我,我忙着转账呢!");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
System.out.println(Thread.currentThread().getName()
+"有内鬼,终止交易!");
//注意此处的break
break;
}
}
System.out.println(Thread.currentThread().getName()
+":啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target,"李四");
System.out.println(Thread.currentThread().getName()
+":让李四开始转账。");
thread.start();
Thread.sleep(10*1000);
System.out.println(Thread.currentThread().getName()
+":老板来电话了,得赶紧通知李四对方是个骗子!");
thread.interrupt();
}
}
运行结果:
上面代码中线程在阻塞的情况下进行终止,则以InterruptedException异常的形式通知,同时会把终止线程时标志位为true的情况下把标志位清除,变为false。
终止线程出现了异常时,标志位被清除,标志位变为false,while循环里面的判断线程状态仍然满足true,继续循环,要想终止线程,取决于catch里面的语句。比如上面的break语句,报出异常时进入catch语句,运行到break,直接结束循环;若没有采取结束线程的措施(比如不用break),循环一直会继续,线程不会终止。
- 示例3:观察标志位是否被清除
使用Thread.interrupted()观察中断状态(线程中断不会清除标志位)
public class ThreadDemo {
private static class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.interrupted());
}
}
}
public static void main(String[] args) {
MyRunnable myRunnable =new MyRunnable();
Thread thread = new Thread(myRunnable,"李四");
thread.start();
thread.interrupt();//调用该函数并不会终止,只是把标志位改变了
}
}
运行结果:
- 使用Thread.currentThread().isInterrupted(),线程中断标记位不会被清除
public class ThreadDemo {
private static class MyRunnable implements Runnable{
@Override
public void run() {
for(int i= 0;i<10;i++){
System.out.println(Thread.currentThread().isInterrupted());
}
}
}
public static void main(String[] args) {
MyRunnable myRunnable =new MyRunnable();
Thread thread =new Thread(myRunnable,"李四");
thread.start();
thread.interrupt();
}
}
运行结果:
3.2.3 等待一个线程——join()
有时我们需要让一个线程等待另一个线程执行结束再继续执行,本质上 就是控制线程结束的顺序。例如,张三只有等到李四转账成功后,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long mills) | 等待线程结束,最多等mills毫秒 |
public void join(long mills,int nanos) | 同理,但是可更高精度 |
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable target =()->{
for(int i =0;i<10;i++){
try{
System.out.println(Thread.currentThread().getName()
+":我正在工作!");
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+":我结束了!" );
};
Thread thread1 = new Thread(target,"李四");
Thread thread2 =new Thread(target,"王五");
System.out.println("先让李四开始工作");
thread1.start();
thread1.join();
System.out.println("李四工作结束了,让王五开始工作!");
thread2.start();
thread2.join();
System.out.println("王五工作结束了!");
}
}
运行结果:
从结果可以看出,通过利用join函数实现了线程依次工作。
在哪一个线程内调用join,就让该线程等待调用join的线程执行结束,如果调用join的线程已经结束了,就直接返回,不会涉及阻塞。
例如下面代码:
public class ThreadDemo {
public static void main(String[] args) {
Thread thread1 =new Thread(()->{
for(int i =0;i<5;i++){
try {
System.out.println("让我第二个来!");
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("我结束了!到下一个了");
});
System.out.println("我是main线程,我先来");
Thread thread2 = new Thread(()->{
try {
thread1.join(); //让tread2线程等待thread1线程
for (int i = 0; i < 5; i++) {
System.out.println("让我第三个来");
Thread.sleep(1000);
}
}catch (InterruptedException e){
e.printStackTrace();
}
});
//线程开始执行
thread1.start();
thread2.start();
System.out.println(Thread.currentThread().getName()+":我先结束了!");
}
}
运行结果:
3.2.4 获取当前线程引用
在上面的例子中该方法也涉及到
方法 | 说明 |
public static Thread currentThread(); | 返回当前线程对象的引用 |
public class ThreadDeno {
public static void main(String[] args) {
//获取线程即main线程
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
3.2.5 休眠当前线程
方法 | 说明 |
public static void sleep(long mills)throws InterruptedException | 休眠当前线程mills毫秒 |
public static void sleep(long mills,int nanos)throws InterruptedException | 可以更高精度的休眠 |
如果线程在睡眠期间被中断,sleep方法会立即返回,并且会抛出InterruptedException。
public class ThreadDeno {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3*1000);
System.out.println(System.currentTimeMillis());
}
}
运行结果:
四、线程的状态
4.1 观察线程的所有状态
线程的状态是一个枚举类型Thread.State
public class ThreadDeno {
public static void main(String[] args) {
for (Thread.State state:Thread.State.values()){
System.out.println(state);
}
}
}
- NEW:线程已经有了,start方法还没调用。
- RUNNABLE:可工作的,又可以分成正在工作中和即将开始工作(就绪状态)。
- BLOCKED:阻塞。由于锁竞争导致的阻塞。
- WAITING:阻塞,由于wai这种不固定时间的方式产生的阻塞。
- TIMED_WAITING:阻塞。由于sleep这种固定时间的方式产生的阻塞。
- TERMINATED:Thread对象还在,内核中的线程已经没了。
4.2线程状态和状态转移的意义
形象的表示上图:
从上面中,回顾之前的isAlive()方法,可以认为是处于不是NEW和TERMINATED的状态都是活着的。
- 使用isAlive方法判定线程的存活状态
public class ThreadStateTransfer {
public static void main(String[] args) {
Thread t= new Thread(()->{
for (int i= 0;i<1000_0000;i++){
}
},"李四");
System.out.println(t.getName()+":"+t.getState());
t.start();
//只要t线程不结束,主线程也不会结束
while(t.isAlive()){
System.out.println(t.getName()+":"+t.getState());
}
System.out.println(t.getName()+":"+t.getState());
}
}
- 关注WAITING、BLOCKED、TIMED_WAITING状态的转换
public class ThreadDemo {
public static void main(String[] args) {
final Object object =new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object){
while (true){
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
},"t1");
t1.start();
Thread t2 =new Thread(new Runnable() {
@Override
public void run() {
synchronized (object){
System.out.println("heheh");
}
}
},"t2");
t2.start();
}
}
使用jconsole可以看到t1的状态是TIMED_WAITINIG,t2的状态是BLOCKED
修改上面的代码,t1中的sleep换成wait
public class ThreadDemo {
public static void main(String[] args) {
final Object object =new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object){
while (true){
try{
object.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
},"t1");
t1.start();
Thread t2 =new Thread(new Runnable() {
@Override
public void run() {
synchronized (object){
System.out.println("heheh");
}
}
},"t2");
t2.start();
}
}
说明:对于wait()方法,执行后会释放锁,但该线程会进入waiting状态。释放锁后t2执行完毕,只有调用了notify()或notifyAll() 后wait才会返回,t1才会重新获取锁继续执行。(这方面的内容后面会讲到)
使用jconsole可以看到t1 的线程状态是WAITING。
结论:
- BLOCKED表示等待获取锁,WAITING和TIMED_WAITING表示等待其他线程发来通知。
- TIMED_WAITING现成在等待唤醒,但设置了时限;WAITING现成在无限等待唤醒。
- yield()大公无私,让出CPU
public class ThreadDemo {
public static void main(String[] args) {
final Object object =new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("张三");
Thread.yield();
}
}
},"t1");
t1.start();
Thread t2 =new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("李四");
}
}
},"t2");
t2.start();
}
}
说明:使用yield()时,表示当前线程愿意让出CPU使用权让另一个线程来执行,而不是强制当前线程被挂起或停止。
可以看到:
- 不使用yield的时候,张三李四大概打印的内容基本一样多。
- 使用yield的时候,大部分打印的都是李四的内容。
结论:
yield不改变线程的状态,但是会重新排队(处于READY状态)。
五、多线程带来的风险——线程安全(重点)
5.1 观察线程不安全
public class ThreadDemo {
static class Counter{
public int count = 0;
void increase(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
//利用final保证该变量不能再指向另一个变量
final Counter counter = new Counter();
Thread t1 = new Thread(()->{
for(int i=0;i<50000;i++){
counter.increase();
}
});
Thread t2 = new Thread(()->{
for (int i=0;i<50000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
预想结果应该加起来是10w,但是执行后的结果却不是这样。
5.2 线程安全的概念
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
5.3 线程不安全的原因
(1)修改共享数据
上面的线程不安全的代码中,涉及到多个线程针对counter.count变量进行修改,此时这个counter.count是一个多个线程都能访问到的“共享数据”。
说明:counter.count这个变量就是在堆上,因此可以被多个线程共享访问。
上面代码中,count++这个操作本质上是分三步进行的:
- load:把数据从内存读到CPU寄存器中
- add:把寄存器中的数据进行+1
- save:把寄存器中的数据保存到内存中
这是执行正确结果的情况:
然而线程是随机调度的, 使这两个线程执行逻辑的先后顺序存在诸多可能。
很显然这段代码并没有满足原子性。
原子性:我们把代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;同时B也进入了房间,打断A在房间里的隐私。这个就是不具备原子性,即保证事务完成的完整性。
对于这样的问题,是不是可以给房间加一把锁,A进去就把门锁上,其他人进不来,这样就满足了原子性。
如果不保证原子性会给多线程带来什么问题:
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果可能是错误的。
(2)可见性
可见性是指,一个线程对共享变量值的修改,能够及时地被其他线程看到。
Java内存模型(JMM):java虚拟机规范中定义了java内存模型
其目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
- 线程之间的共享变量存在主内存(Main Memory)。
- 每一个线程都有自己的“工作内存”。
- 当线程读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据。
- 当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,再同步会主内存。
由于每个线程有自己的工作内存,这些工作内存中的内容相当于同一个共享变量的“副本”,此时修改线程1的工作内存的值时,线程2的工作内存不一定会及时变化。
- 1)初始情况下,两个线程的工作内存内容一致
- 2)一旦线程1修改了a的值,此时主内存不一定能及时同步,对应的线程2的工作内存的a的值也不一定能及时同步。
这个时候代码中就容易出现问题。
此时引入了两个问题:
- 为啥要整这么多内存?
- 为啥要这么麻烦的拷来拷去?
1)为啥整这么多内存?
实际并没有这么多“内存”,这只是java规范中的一个术语,是属于“抽象”的叫法。
所谓的“主内存”才是真正硬件角度的“内存”,而所谓的“工作内存”,则是指CPU的寄存器和高速缓存。
2)为啥要这么麻烦的拷来拷去?
因为CPU访问自身寄存器的速度以及高速缓存的速度,远远超过访问内存的速度(快了3~4个数量级,也就是几千倍,上万倍)。
比如某个代码中要连续10次读取某个变量的值,如果10次都从内存读,速度是很慢的。但是如果只是第一次从内存读,读到的结果缓存到CPU的某个寄存器中,那么后9次读数据就不必直接访问内存了,效率就大大提升了。
那么问题又来了,既然访问寄存器速度这么快,还要内存干啥?
答案就是一个字:贵!!!
值得一提的是,块和慢都是相对的,CPU访问寄存器速度远远快于内存,但是内存的访问速度有远远快于硬盘。
对应的,CPU的价格最贵,内存次之,硬盘最便宜。
(3)可见性和原子性
- 可见性问题通常是因为线程的工作内存和主内存的同步问题(线程之间的缓存机制)。比如,线程A修改了共享变量,但线程B并没有看到修改的结果。可见性保证是确保线程能够正确地看到其他线程对共享数据的修改(典型的是一个线程读,一个线程改)。
- 原子性问题通常是由于多个线程同时访问共享数据,并进行修改,可能发生数据冲突或不一致。原子性就是为了保证对共享数据在多个线程之间的操作不会发生干扰。
(4)代码顺序性
什么是代码重排序?
比如一段代码是这样的:
1.去客厅拿吃的
2.去卧室里睡觉
3.去客厅喝水
如果在单线程的情况下,JVM,CPU指令集会对其进行优化,比如,按1->3->2的方式执行,也是没问题,可以少跑一次卧室,这种就叫做指令重排序,理解重在重排序。
注意:编译器对于指令重排序的前提是“保持逻辑不发生变化”,这一点在单线程环境下比较容易判断,但是在多线程环境下就没那么容易了 ,多线程代码执行的复杂程度高,编译器很难在编译阶段对代码的效果进行预测,因此激进的重排序很容易导致优化后的逻辑和之前不等价,比如可能会执行其他指令。
5.4 解决上面的线程不安全问题
这里用到的机制,后面会进行解释:
public class demo {
static class Counter{
public int count = 0;
synchronized void increase(){
count++;
}
}
//这样也可
/* void increase(){
synchronized (this) {
count++;
}
}
*/
public static void main(String[] args) throws InterruptedException {
final Counter counter =new Counter();
Thread t1 = new Thread(()->{
for (int i=0;i<50000;i++){
counter.increase();
}
});
Thread t2 =new Thread(()->{
for (int i=0;i<50000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
或者这样:
public class demo {
static class Counter{
public int count = 0;
void increase(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter =new Counter();
Object lock =new Object();
Thread t1 = new Thread(()->{
for (int i=0;i<50000;i++){
synchronized (lock) {
counter.increase();
}
}
});
Thread t2 =new Thread(()->{
for (int i=0;i<50000;i++) {
synchronized (lock) {
counter.increase();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
输出结果:
六、synchronized关键字-监视器锁monitor lock
6.1 synchronized的特性
1)互斥
synchronized会起到互斥的效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待。
- 进入synchronized修饰的代码块,相当于加锁
- 退出synchronized修饰的代码块,相当于解锁
进入方法内部,相当于针对当前对象“加锁”,方法执行完毕相当于解锁。上面的synchronized不仅可以修饰一个实例方法,还可以修饰一个代码块和静态方法。
此处两个线程共用同一个锁对象,当第一个线程执行时对该线程加锁,t1和t2发生锁竞争,t2要等到t1执行完,释放锁之后才能执行。
说明:锁竞争,如果两个线程是在针对同一个对象加锁,就会有锁竞争,如果不是针对同一个对象加锁,就不会有锁竞争,仍然是并发执行、
比如去公厕上厕所,如果只有一个厕所,当你进去后,把门锁上了,后面的人要等到你出来后把锁释放了后面的人才能用;如果有多个厕所,你进去其中一个厕所后,其他人也可以进去其他厕所,互不干扰。
synchronized用的锁是存在java对象里头的。
可以粗略理解成,每个对象在内存中存储的时候,都存有一块内存表示当前的“锁定”状态(类似于厕所的“有人/无人”)。
如果当前是“无人”状态,那么就可以使用,使用时需要设为“有人”状态。
如果当前是“有人”状态,那么其他人无法使用,只能排队。
理解“阻塞等待”
针对每一把锁,操作系统内部维护了一个等待队列(后面会讲)。当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直等到之前的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁。
注意:
- 上一个线程解锁之后,下一个线程并不是立即就能获取到锁,而是要靠操作系统来“唤醒”,这也就是操作系统线程调度的一部分工作。
- 假设有A,B,C三个线程,线程A先获取到锁,然后B尝试获取锁,然后C再尝试获取锁,此时B和C都在阻塞队列中排队,但是当A释放锁之后,虽然B比C先来的,但是B不一定就能获取到锁,而是和C重新竞争,并不遵守先来后到的规则。
2)刷新内存
synchronized的工作过程(存疑!网上很多版本):
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存中
- 释放互斥锁
所以synchronized也能保证内存的可见性,具体代码参见后面的volatile部分。
3)可重入
可重入是synchronized的重要特性,即一个线程,连续针对一把锁,加锁两次不会出现死锁。synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
“把自己锁死”:
一个线程没有释放锁,然后又尝试再次加锁
//第一次加锁,加锁成功
lock();
//第二次加锁,锁已经被占用,阻塞等待
lock();
按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待,直到第一次的锁被释放,才能获取到第二个锁。但是释放第一个锁也是由该线程来完成的,结果这个线程已经躺平了,啥都不想干了(即进入了一种停滞或不再继续执行其他操作,比如死循环等),也就无法进行解锁操作,这时候就会死锁。像这样的锁称为不可重入锁。
代码示例:
public class Counter{
public int count = 0;
synchronized void increase(){
count++;
}
synchronized void increase1(){
increase();
}
}
在上面代码中,
- increase 和increase1两个方法都加了synchronized,此处的synchronized都是针对当前对象加锁的。
- 在调用increase1的时候,先加了一次锁,执行到increase1的时候又加了一次锁。(上个锁还没有释放,相当于连续加两次锁)。
这个代码是没问题的,因为synchronized是可重入锁。
在可重入锁的内部,包含了“线程持有者”和“计数器”两个信息。
- 如果某个线程加锁的时候,发现已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并让计数器自增。
- 解锁的时候计数器递减为0的时候,才真正释放锁(才能被别的线程获取到)。
6.2 synchronized使用示例
synchronized本质上要修改指定对象的“对象头”。从使用角度来看,synchronized也势必要搭配一个具体的对象来使用。
1)直接修饰普通方法:锁的synchronizedDemo对象:
public class SynchronizedDemo{
public synchronized void method(){
}
}
2)修饰静态方法:锁的SynchronizedDemo类的对象
public class synchronizedDemo{
public synchronized static void method(){
}
}
3)修饰代码块:明确指定哪个对象
锁当前对象:
public class synchronizedDemo{
public void method(){
synchronized(this){
}
}
}
锁类对象:
public class SynchronizedDemo{
public void method(){
synchronized(Synchronized.class){
}
}
}
我们重点理解,synchronized锁的是什么,两个线程竞争同一把锁,才会产生阻塞等待。
两个线程分别尝试获取两把不同的锁,不会产生竞争。
6.3 关于死锁
死锁的成因涉及四个必要条件:
- 互斥使用(锁的基本特性):当一个线程持有一把锁之后,另一个线程也想获取到锁,就要阻塞等待;
- 不可抢占(锁的基本特征):当锁已经被线程1拿到之后,线程2只能等线程1主动释放,不能强行抢过来。
- 请求保持(代码结构):一个线程尝试获取多把锁(先拿到锁1之后,在尝试获取锁2,获取的时候,锁1不会释放,即锁1释放不了);
- 循环等待/环路等待(代码结构):Thread1首先获取lock1,然后尝试获取lock2,而Thread2首先获取了lock2,然后尝试获取lock1,双方线程都在等待对方的锁释放,就会陷入等待的依赖关系,形成环。
解决死锁核心就是破坏上述必要条件,只要破坏一个死锁就形成不了:
- 1和2破坏不了(synchronized 自带特性,无法干预);
- 对于3来说,调整代码结构,避免编写“锁嵌套”逻辑;
- 对于4来说,可以约定加锁的顺序,就可以避免循环等待。
七、volatile关键字(保证内存可见性)
计算机访问的数据往往存储在内存中,CPU使用这个变量的时候,就会把内存中的数据读出来然后放到CPU寄存器中再参与运算。而CPU读取内存的操作非常慢(相对的),为了解决这个问题,提高效率,此时编译器就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器,减少了读内存的次数,提高了效率。
问题代码(一个线程读,一个线程改):
public class ThreadDemo {
private static int isquit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(isquit == 0){
//空
}
System.out.println("t1 退出!");
});
t1.start();
Thread t2 = new Thread(()->{
System.out.println("请输入isquit:");
Scanner scanner = new Scanner(System.in);
isquit = scanner.nextInt();
});
t2.start();
}
}
运行结果:
可以看,当输入1的时候程序并没有结束。
通过jconsole也能看到,t1线程正在执行,RUNNABLE状态。
此处的问题是“内存可见性”情况引起的!
问题来源:
线程t1从内存中读取isquit到寄存器中,通过cmp指令比较寄存器的值是否是来决定是否要继续循环,由于这个循环速度飞快,短时间内进行了大量的操作,编译器发现每次加载的数据到寄存器中都是一样的,所以编译器在第一次循环的时候才读了内存,后续都不再读内存了,而是从寄存器(工作内存)中读取了。
后续另一个线程修改isQuit的时候,t1感觉不到isquit变量的变化。
利用volatile关键字之后:
public class ThreadDemo {
private static volatile int isquit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(isquit == 0){
//空
}
System.out.println("t1 退出!");
});
t1.start();
Thread t2 = new Thread(()->{
System.out.println("请输入isquit:");
Scanner scanner = new Scanner(System.in);
isquit = scanner.nextInt();
});
t2.start();
}
}
运行结果:
当这个isquit被关键字volatile修饰后,一个线程改变了自己工作内存中的该isquit(共享变量)后,会立即被更新到主内存中,同时其他线程工作内存中的isquit副本变为失效状态,当其他线程再次读取或者修改该变量时,会直接从主内存中读取最新的值。
加上了volatile,虽然强制读写内存速度是变慢了,但是数据变得更准确了。
7.1 volatile特点
- 写操作不重排序:在写入一个volatile变量时,它之前的所有操作都将在写入发生之前完成;
- 读操作不重排序:当volatile变量读取值时,任何读取之后的操作都必须等到读取完成之后才能开始;
- volatile变量的写入对所有线程立即可见:对volatile变量的写操作确保了新值对所有其他线程立即可见。
7.2 volatile不保证原子性:
volatile和synchronized有着本质的区别,synchronized能够保证原子性,volatile保证的是内存可见性 ,保证原子性体现的是线程的调度和执行顺序,而可见性问题涉及的是内存和工作内存(或CPU缓存)之间的同步。
下面是之前线程安全问题的代码,加上volatile后:
public class ThreadDemo {
static class Counter{
volatile public int count = 0;
void increase(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
//利用final保证该变量不能再指向另一个变量
final Counter counter = new Counter();
Thread t1 = new Thread(()->{
for(int i=0;i<50000;i++){
counter.increase();
}
});
Thread t2 = new Thread(()->{
for (int i=0;i<50000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
7.3 synchronized也能保证内存可见性
synchronized既能保证原子性,也能保证内存可见性。
对上面的代码进行调整:
给t1的循环内部加上synchronized,并借助counter对象加锁。
public class ThreadDemo {
static class Counter{
private static int isquit = 0;
}
public static void main(String[] args) {
Counter counter =new Counter();
Thread t1 = new Thread(()->{
while (true) {
//空
synchronized (counter) {
if (counter.isquit !=0){
break;
}
}
}
System.out.println("t1 退出!");
});
t1.start();
Thread t2 = new Thread(()->{
System.out.println("请输入isquit:");
Scanner scanner = new Scanner(System.in);
counter.isquit = scanner.nextInt();
});
t2.start();
}
}
运行结果:
可以看到,输入1后结束了循环, 一旦线程t2更新了isquit的值,并且t1线程再次进入synchronized同步块,由于synchronized块保证内存可见性,t1将看到isquit的最新值,并据此结束循环。
八.wait和notify
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知。
但是实际开发中有时候我们先希望合理的协调多个线程之间执行的先后顺序。
要完成线程的协调工作,主要涉及到三个方法:
- wait()/wait(long timeout):让当前线程进入等待状态。
- notify()/notifyAll():唤醒在当前对象上的等待的线程。
- 这三者都是用锁对象进行调用的。
注意:wait,notify,notifyAll都是Object类的方法。
8.1 wait()方法
wait做的事情:
- 使当前执行代码的线程进行等待(阻塞)。(把线程放到等待队列中)
- 释放当前的锁。
- 满足一定条件时被唤醒,重新尝试获取这个锁。
wait要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常。
wait结束等待的条件:
- 其他线程调用该对象的notify方法。
- wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定等待时间)。
- 其他线程调用该等待线程的interrupted方法,导致wait()抛出InterruptedException异常。
代码示例:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Object object =new Object();
synchronized (object){
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
}
运行结果:
这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到notify方法进行唤醒。
8.2 notify()方法
notify方法是唤醒等待的线程。
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的线程,对其发出notify通知,并使它们重新获取该对象的对象锁。
- 如果有多个线程,则由线程调度器随机挑选出一个呈wait状态的线程。(并没有“先来后到”)
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
代码示例:使用notify()方法唤醒线程
- 创建WaitTask类,对应一个线程,run内部循环调用wait。
- 创建NotifyTask类,对应另一个线程,在run内部调用一次notify。
- 注意,WaitTask和NotifyTask内部持有同一个Object lock。WaitTask和NotifyTask要想配合就需要搭配同一个Object。
public class TreadDemo {
static class waitTask implements Runnable{
private Object locker;
public waitTask(Object locker){
this.locker = locker;
}
public void run(){
synchronized (locker){
while(true){
try{
System.out.println("开始");
locker.wait();
System.out.println("wait结束");
break;
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
static class NotifyTask implements Runnable{
private Object locker;
public NotifyTask(Object locker){
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify开始");
locker.notify();
System.out.println("notify结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker =new Object();
Thread t1 = new Thread(new waitTask(locker));
Thread t2 =new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}
}
运行结果:
8.3 notifyAll()方法
notify方法只是唤醒某一个等待线程,使用notifyAll方法可以一次唤醒所有的等待线程。
范例:使用notifyAll()方法唤醒所有等待线程,在上面的代码基础上做出修改。
- 创建3个WaitTask实例。1个NotifyTask实例。
public static void main(String[] args) throws InterruptedException {
Object locker =new Object();
Thread t1 = new Thread(new waitTask(locker));
Thread t2 =new Thread(new waitTask(locker));
Thread t3 = new Thread(new waitTask(locker));
Thread t4 = new Thread(new NotifyTask(locker));
t1.start();
t2.start();
t3.start();
Thread.sleep(1000);
t4.start();
}
运行结果:
此时可以看到,调用notify只能唤醒一个线程,并且其他线程并未结束,程序也就没有结束。
- 修改NotifyTask中的run方法,把notify替换成notifyAll
@Override
public void run() {
synchronized (locker) {
System.out.println("notify开始");
locker.notifyAll();
System.out.println("notify结束");
}
}
运行结果:
此时可以看到,调用notifyAll能同时唤醒3个wait中的线程,这三个线程结束了,程序也随之结束了。
注意:虽然是同时唤醒3个线程,但是这3个线程需要竞争锁。所以并不是同时 执行,而仍然是有先有后的执行。
8.4 wait()和sleep的对比
其理论上wait和sleep完全是没有可比性的因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。
其他区别:
- wait需要搭配synchronized使用,sleep不需要。
- wait是Object的方法,sleep是Thread的静态方法。
8.5 线程饿死
何为线程饿死?
是指在多线程中,某些线程没能获取系统资源或调度执行的机会,导致进入阻塞状态,这种情况属于概率问题,一般线程死锁出现的概率远大于饿死出现的概率,想模拟出来比较难。
解决线程饿死问题:
搭配wait和notify来解决。
好啦,线程相关知识就先介绍到这里,后续会更新多线程的相关案例,感谢支持!