join
我们可以通过使用join()
方法来进行线程等待的操作:
public class Demo2 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println("t1 线程启动"); }); Thread t2 = new Thread(() -> { System.out.println("t2 线程启动"); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Main 线程启动"); }}
上面前两个方法我们经常使用到,第一个方法在上面已经演示过了,第二个方法是设置了时间,也就是最多等待 x 毫秒就不会继续等待了,如果一个线程因为有些原因而始终处于阻塞状态的话,为了避免程序持续运行,我们会设置一下等待时间,避免整个程序都挂了。
后面的方法则是设置了 ns 级别,但是我们很少会用到,因为我们大部分人的电脑的操作系统都不是实时操作系统的,由于线程存在调度,也就是线程调度过来了并不会立即就会执行可能会晚几纳秒这样的,但是这种延迟我我们是可以接受的,所以最后一个方法我们几乎不会使用。
现在来处理一下谁等谁的问题,上面的代码中我们实在 main 方法里使用了 t1.join() 和 t2.join(),也就是 main 线程等 t1 和 t2 线程的结束,我们才执行 main 线程
也就是说在哪个线程里使用 xxx.join() ,意味着 这个线程 等 xxx
我们也可以让 t1 线程等 main 线程:
public class Demo2 { public static void main(String[] args) throws InterruptedException { Thread cur = Thread.currentThread(); Thread t1 = new Thread(() -> { try { cur.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("t1 线程启动"); }); t1.start(); for (int i = 0; i < 3; i++) { System.out.println("Hello Main"); } }}
线程的状态
new: 表明线程对象被创建出来了
runnable:说明线程正在执行(执行状态)或者处于要执行的状态(就绪状态)
wating:表明线程正在等待某些事情,例如上面的 join() 方法,一个线程等待另一个线程的结束。
time_wating:表明线程正在等待某些事情,但是是由等待时间限制的,也就是说,这不是死等,时间一到,这个线程就会被执行,例如上面的 join(ms) 方法,一个线程等待另一个线程 x 毫秒。
blocked:说明线程此时处于阻塞状态,这个阻塞状态是由于没有获取到锁而陷入的阻塞状态,和上面提到的 wating 和 time_wating 是由区别的。
terminated:表明线程终止。可以是正常执行完的终止也可以是我们手动的终止。
上一篇文章中我们学过的 isAlive() 方法,可以认为是 线程处于不是NEW和TERMINATED的状态 都是活着的。
synchronized
引入
public class Demo3 { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count = " + count); }}
大家来思考一下运行结果是什么?
你会发现每一次运行的结果都是不一样的。
我们站在CPU 的角度来分析上面的代码,首先 count++ 是有三条指令,首先加载 count 的数值,然后进行 +1,最后保存 count 的数值。
由于线程的调度是随机的就会出现下面的情况,这里我举一个例子供大家参考:
在上面的调度中,t1 先加载 count = 0, 然后 t1 被调度走了,t2 带哦都过来了,此时 t2 也执行了 加载 count = 0; 然后 t2 被调度走了,此时 t1调度过来将 count + 1 = 1, 之后又被调度走了,t2 来了执行 add ,count + 1 = 1,然后进行 save 保存数据,此时 count 被修改为 1,然后 t1 调度过来了,进行 save 操作,这时候将 count 修改为 1,之后进行保存
此时你会发现,上面进行了两个 ++ 操作,但是 count 却变为了 1,这就是线程安全问题
我们再来分析一下,我们最后得到的count 的范围有可能为 10 w 吗?
答案是有可能的,但是概率极低,只要每一个线程的三条指令都是串行执行的话,就会得到正确的结果 10w
那count 有可能小于 5 w 吗?
有可能,假设一共有 5 次 ++ 操作,t1 线程执行 3 次,t2 线程执行 2 次:
5 次 ++ ,最后得到 2
10 w 次++ ,最后是可能得到小于 5 w 的结果的,但是这个概率极低
synchronized 的使用
为了避免上面的线程安全问题,我们可以进行加锁操作,Java 给我们提供了 synchronized
关键字,让我们可以对代码进行加锁操作:
public class Demo3 { private static int count = 0; public static void main(String[] args) throws InterruptedException { Object locker = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized(locker) { count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized(locker) { count++; } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count = " + count); }}
synchronized(对象) { //需要加锁的代码}
一般情况下,我们的 synchronized 的括号内部需要传入一个锁对象,这个所对象可以为任意对象,但是这里建议大家自己创建一个专门用来作锁的对象,专象专用
在Java中,如果两个及以上的线程对同一个对象都有加锁需求的话,当谁拥有这个对象的锁的时候,就可以进行sychronized 里面的操作了,如果没有拥有这个对象的锁,就会处于阻塞状态,直到获得这个对象的锁。
这里需要注意如果加锁的对象不是同一个的话,那么这些线程是不会发生互斥关系的
只有当两个及以上的线程对同一个对象加锁的时候,才会产生互斥(锁冲突 / 锁竞争)
synchronized 的其他用法:
可以修饰方法:
class Test { private static int count = 0; synchronized public void add() { count++; } public int getCount() { return count; }}public class Demo4 { public static void main(String[] args) throws InterruptedException { Test test = new Test(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { test.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { test.add(); } }); t1.start(); t2.start(); t1.join();; t2.join(); System.out.println("count = " + test.getCount()); }}
你也可以在方法里面写 synchronized:
public void add() { synchronized(this) { count++; } }
括号里面记得写 this,表示当前加锁的对象
实质上这种写法其实和上面直接对类的方法加 synchronized 是一样的,都是对 this 加锁
当然也可以对进行方法使用 synchronized ,这样就是对类对象进行加锁
class Counter { private static int count = 0; synchronized public static void add() { count++; } public static int getCount() { return count; }}public class Deno5 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { Counter.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { Counter.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count =" + Counter.getCount()); }}
synchronized 的可重入性
public class Demo1 { public static void main(String[] args) { Object locker = new Object(); Thread t1 = new Thread(() -> { synchronized(locker) { synchronized (locker) { System.out.println("t1 线程启动"); } } }); t1.start(); }}
我们看一下上面的代码,你会发现一个线程内部对一个对象进行了两次加锁,我们回到锁的性质来看,当一个对象被加锁之后,其他线程要想获得这个锁就必须阻塞等待,那么在这个线程里,按照理论来说,这个线程会陷入死锁状态,第一次已经对 locker 加过锁了,第二次就应该是无法获得 locker 这个锁对象了,但是事实并非如此:
线程依然能正常执行,这是因为Java 的synchronized 带有可重入特性,简单来说,就是在同一个线程里,可以对已经获得的锁对象进行重复加锁,这样就避免因为一个线程对同一个对象重复加锁产生的死锁问题了。
在可重入下,什么时候解锁?
当然是第一个synchronized 的最后一个括号出来就会自动解锁,因为两个synchronized 之间可能存在代码,加锁加锁就一次性锁住
如何实现可重入锁?
可重入特性是在同一个线程对同一个锁对象进行加锁,那么我们只要在锁内部记录当前是哪个线程持有这把锁,在后续加锁的时候,就进行判定。
使用计数器来统计括号,遇到左括号就++, 遇到右括号就–,等到 count == 0 的时候就直接解锁
不要认为 JVM 分不出是不是 synchronized 的括号,在 .java 编译成 .class 字节码文件的时候,不同的括号都有不同的含义,对应的字节码是不一样的。