目录
线程状态
观察线程的所有状态
线程状态及其描述
线程状态转换
代码示例1
代码示例2
线程安全
概念
线程不安全的代码示例
线程不安全的原因
线程安全的代码示例-加锁
synchronized关键字
synchronized的特性
小结
形成死锁的四个必要条件
synchronized的使用示例
Java标准库中的线程安全类
线程状态
观察线程的所有状态
线程的状态是一个枚举类型 Thread.State
public class Demo11 { public static void main(String[] args) { for(Thread.State state:Thread.State.values()) System.out.println(state); }}
运行结果
线程状态及其描述
NEW:Thread对象已经有了,但是start方法还没调用;
RUNNABLE:就绪状态,线程已经在CPU上执行了/线程正在排队等待执行(即工作中或即将开始工作);
TERMINATED:Thread对象还在,但是内核中的下线程已经没了,即工作完成了;
TIMED_WARTING:阻塞状态,由于sleep这种固定时间的方式产生的阻塞;
WAITING:阻塞,由于wait这种不固定时间的方式产生的阻塞;
BLOCKED:阻塞,由于锁竞争导致的阻塞。
线程状态转换
代码示例1
public class Demo11 { public static void main(String[] args) { 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) { throw new RuntimeException(e); } } } } },"t1"); t1.start(); Thread t2=new Thread(new Runnable() { @Override public void run() { synchronized (object){ System.out.println("hello"); } } },"t2"); t2.start(); }}
通过jconsole可以看到t1的状态是TIMED_WAITING,t2的状态是BLOCKED。
代码示例2
public class Demo11 { public static void main(String[] args) { Object object=new Object(); Thread t1=new Thread(new Runnable() { @Override public void run() { synchronized (object){ while(true){ try { object.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } },"t1"); t1.start(); }}
通过jconsole可以看到t1的状态是WAITING.
小结
BLOCKED表示等待获取锁,WAITING和TIMED_WAITING表示等待其它线程发来通知;
TIMED_WAITING线程在等待唤醒,但设置了时限;
WAITING线程在无限等待唤醒。
线程安全
概念
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
线程不安全的代码示例
public class Demo12 { 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); }}
运行结果
结果与预期结果不一致且差别很大,显然上述代码是线程不安全的。
线程不安全的原因
1.修改共享数据
上述代码涉及到多线程(两个及两个以上的线程)针对同一个变量count进行修改。
2.原子性
一条Java语句不一定是原子的,也不一定只是一条指令。
比如count++,其实是由三步操作组成的:
1.从内存中把数据读取到CPU;
2.对变量count进行++;
3.把数据写回到内存。
如果一个线程正在对一个变量操作,中途其它线程插入进来了,如果这个操作被打断,结果就可能是错误的。
3.可见性
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.
Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.
线程之间的共享变量存在 主内存 (Main Memory).
每一个线程都有自己的 "工作内存" (Working Memory) .
当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 "副本". 此时修改线程1 的工作内存中的值, 线程2 的工作内存不一定会及时变化.
4.指令重排序
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,原来是按1->2->3的方式执行,优化后可能会按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序。
编译器对于指令重排序的前提是 "保持逻辑不发生变化". 这一点在单线程环境下比较容易判断, 但
是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代
码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.
线程安全的代码示例-加锁
public class Demo12 { 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的特性
1)互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁。
退出 synchronized 修饰的代码块, 相当于 解锁。
synchronized用的锁是存在Java对象头里的。
synchronized 的底层是使用操作系统的 mutex lock 实现的 .2)可重入
Java 中的 synchronized 是 可重入锁 ,synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
理解死锁
一个线程没有释放锁 , 然后又尝试再次加锁 .// 第一次加锁, 加锁成功lock();// 第二次加锁, 锁已经被占用, 阻塞等待. lock();
按照之前对于锁的设定 , 第二次加锁的时候 , 就会阻塞等待 . 直到第一次的锁被释放 , 才能获取到第 二个锁 . 但是释放第一个锁也是由该线程来完成 , 结果这个线程已经躺平了 , 啥都不想干了 , 也就无 法进行解锁操作 . 这时候就会 死锁 . 代码示例 static class Counter { public int count = 0; synchronized void increase() { count++; } synchronized void increase2() { increase(); }}
在上面的代码中, increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释
放, 相当于连续加两次锁),这个代码是完全没问题的. 因为 synchronized 是可重入锁.
小结
在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
形成死锁的四个必要条件
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
1.互斥条件(锁的基本特性)
当一个线程持有一把锁之后,另一个线程也想要获取到锁,就要阻塞等待。
2.不可抢占条件(锁的基本特性)
当锁已经被线程1拿到之后,线程2只能等线程1主动释放,不能强行抢过来。
3.请求与保持条件(代码结构)
一个线程尝试获取多把锁,已经获取到部分数量的锁,但仍尝试获取其它线程已经占有的锁。
4.循环等待/环路等待(代码结构)
等待的依赖关系,形成了环。
这四个条件同时满足时,系统中就可能发生死锁。
解决死锁的方法通常包括死锁预防、死锁避免、死锁检测和死锁恢复等策略。
比如包括调整代码结构,避免循环等待;对锁进行编号,先加编号大的锁或编号小的锁。
synchronized的使用示例
synchronized 本质上要修改指定对象的 "对象头". 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.
1.直接修饰普通方法
public class SynchronizedDemo { public synchronized void methond() { }}
2.直接修饰静态方法
public class SynchronizedDemo { public synchronized static void method() { }}
3.修饰代码块
锁当前对象
public class SynchronizedDemo { public void method() { synchronized (this) { } }}
锁类对象
public class SynchronizedDemo { public void method() { synchronized (SynchronizedDemo.class) { } }}
我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产生竞争.
Java标准库中的线程安全类
Java 标准库中很多都是线程不安全的 . 这些类可能会涉及到多线程修改共享数据 , 又没有任何加锁措施 . ArrayList LinkedList HashMap TreeMap HashSet TreeSet StringBuilder 但是还有一些是线程安全的 . 使用了一些锁机制来控制 . Vector ( 不推荐使用 ) HashTable ( 不推荐使用 ) ConcurrentHashMap StringBuffer我们可以看到,例如StringBuffer类的成员,有不少是加锁的: