第二章:线程安全、内置锁、线程通信
- 线程安全问题
- 什么是线程安全性问题
- 如何解决线程安全性问题
- 线程安全问题实例:SpringBean单例
- Synchronized内置锁
- synchronized同步方法
- synchronized同步代码块
- 两者区别和联系
- 静态synchronized同步方法
- 生产者和消费者问题
- 线程间通信
- 通知等待机制
- wait,sleep和join
- join的底层
1.线程安全问题
1.1什么是线程安全性问题
多线程同时对一个全局变量做写的操作,可能会受到其他线程的干扰,就会发生线程安全性问题
- 全局变量:JMM中共享内存的资源
- 写操作:修改操作
public class ThreadCount implements Runnable{
public Integer count=0;
@Override
public void run() {
for (int i = 0; i <= 5000; i++) {
count++;
System.out.println(Thread.currentThread().getName()+"count:"+count);
}
}
public static void main(String[] args) {
ThreadCount threadCount=new ThreadCount();
Thread thread=new Thread(threadCount);
Thread thread1=new Thread(threadCount);
thread.start();
thread1.start();
}
}
本应该得到10002的结果,但是到了9876就结束了,原因分析:
会出现多个线程间同时修改共享变量,涉及到一个自增运算符线程不安全的问题
自增运算符是一个复合操作,至少包含三个JVM指令(涉及到JMM的知识)
- “内存取值”
- “寄存器加一”
- “存值到内存”
三条指令在JVM内部都是独立进行的,具有原子性,中间完全可能存在多个线程并发执行。
1.2如何解决线程安全性问题
核心思想:上锁(在分布式环境下 用分布式锁)
在同一个JVM中,多个线程需要竞争锁的资源,最终只能够有一个线程能够获取到锁,多个线程同时抢同一把,谁(线程)能够获取到锁,谁就能执行到改代码,如果没有获取锁成功,中间需要经历锁的升级过程。如果线程一直没有获取到锁,就会一直阻塞等待。
如果线程A获取到锁,但一直不释放锁。线程B一直获取不到锁,这会一直阻塞等待
关于临界资源,临界区
临界区资源,即受保护的对象,指代全局共享变量
临界区代码:是访问修改临界资源的代码(为了保证线程安全,要对这个临界区代码进行上锁)
解决线程安全性问题的方法(面试)
1.使用synchronized锁,在JDK1.6开始 内置锁有了锁升级的过程,大大提升了内置锁的性能
2.使用Lock锁(JUC包的),需要自己实现锁升级过程,底层通过AQS和CAS实现
3.通过ThreadLocal线程独占资源,需要注意内存泄漏问题
4.原子类(Atmic开头)CAS非阻塞式
1.3线程安全问题实例:Spring的单例Bean
像SpringMVC的Controller默认为单例,需要注意线程安全问题
为什么SpringBean单例会出现安全性问题(这里只考虑单体应用)
@RestController
@Slf4j
//@Scope(value = "prototype") 注明当前为单例,其实单例也是默认的
public class CountService {
private int count = 0;
//出现全局共享变量,可能当前JVM的多个线程对该变量进行并发访问、修改
@RequestMapping("/count")
//解决:加锁。
public synchronized String count() {
try {
log.info(">count<" + count++);
try {
Thread.sleep(3000);
} catch (Exception e) {
}
} catch (Exception e) {
}
return "count";
}
}
1.4字节码层面分析线程安全性问题
线程安全问题从底层无非就是:
- 多条字节码指令非原子性操作
- 线程的上下文切换导致 共享资源 被非原子性操作 随意修改
- JMM内存模型,共享内存和线程私有内存 之间共享变量的fetch、push
2.内置锁
java对象都隐含有一把锁,即Java内置锁(对象锁、隐式锁)。使用synchronized调用相对于获取syncObject的内置锁
2.1 synchronized同步方法
synchronized 关键字是 Java 的保留字,当使用 synchronized 关键字修饰一个方法的时候,synchronized位于返回类型前,该方法被声明为了同步方法
public class SynchronizedFunc implements Runnable{
public Integer count=0;
//临界代码区
public synchronized void selfPlus(){
count++;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
selfPlus();
System.out.println(Thread.currentThread().getName()+"count"+count);
}
}
public static void main(String[] args) {
SynchronizedFunc syn =new SynchronizedFunc();
new Thread(syn).start();
new Thread(syn).start();
}
}
任何时间只允许一条线程进入同步方法(临界区代码段),如果其他线程都需要执行同一个方法,那么对不起,其他的线程只能等待和排队。
2.2synchronized同步代码块
为了执行效率,最好将同步方法分为小的临界代码段(减小锁粒度),通过代码块则得以解决
synchronized(syncObject) //同步块而不是方法
{
//临界区代码段的代码块
}
syncObjec进入临界区代码段需要获取 syncObject 对象的监视锁(Java对象都有一把Monitor(监视锁))
//之前的代码可以改写为
@Override
public void run() {
for (int i = 0; i < 50; i++) {
synchronized (this.sumLock){
count++;
}
System.out.println(Thread.currentThread().getName()+"count"+count);
}
}
2.3synchronized 方法和 synchronized 同步块
- 锁粒度不同
synchronized 方法是一种粗粒度的并发控制,某一时刻只能有一条线程执行该 synchronized 方法;synchronized 代码块则是一种细粒度的并发控制,处于 synchronized 块之外的其他代码,是可以被多条线程并发访问的。
- 联系:在 Java 的内部实现上,synchronized方法实际上等同于用一个 synchronized 代码块
synchronized 代码块的括号中传入 this 关键字,使用 this 对象锁作为进入临界区的同步锁。
//版本一,使用 synchronized 代码块进行方法内部全部代码的保护,具体代码如下:
public void plus() {
synchronized(this){ //进行方法内部全部代码的保护
amount++;
} }
//版本二,synchronized 方法进行方法内部全部代码的保护,具体代码如下:
public synchronized void plus() {
amount++;
}
2.4 静态的同步方法
Java的对象有两种:Object实例对象和Class对象(类被加载到方法区时,都会为其创建一个Class对象,对于一个类来说,其Class对象也是唯一的)
普通的 synchronized 实例方法,其同步锁是当前对象 this 的监视锁。那么,如果某个
synchronized 方法是 static 静态方法,而不是普通的对象实例方法,static的同步锁则是Class对象,比如下例的对象锁则为 StaticSync.class
public class StaticSync extends Thread{
private static Integer amount=0;
public static synchronized void selfPlus(){
for (int i = 0; i < 10; i++) {
amount++;
System.out.println(Thread.currentThread().getName()+"---amount"+amount);
}
}
@Override
public void run() {
selfPlus();
}
public static void main(String[] args) {
new StaticSync().start();
new StaticSync().start();
}
}
使用 synchronized 关键字修饰 static 静态方法时,一个 JVM 内所有争用线程共用一把锁,是非常粗粒度的同步机制。但如果使用对象锁,并且 JVM 内的争用线程所争用的,是不同对象锁,则争用线程可以同步进入临界区,锁的粒度就变细;
总结:
1.修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码快前要获得 给定对象 的锁。
2.修饰实例方法,作用于当前实例加锁,进入同步代码前要获得 当前实例 的锁
3.修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码前要获得 当前类对象 的锁
2.5生产者与消费问题(版本一)
- 临界资源:一个最多容纳10份烤鸭的盘子
- 生产者:厨师每次往盘子放1份烤鸭(线程:Produce)
- 临界代码区:放一份烤鸭的操作
- 消费者:3位每次吃1分烤鸭的食客(线程:Consumer)
- 临界代码区:拿一份烤鸭的操作
要保证临界资源被并发修改时的线程安全问题,通过Synchronized实现
public class Consumer implements Runnable{
private Plate plate;
public Consumer(Plate plate) {
this.plate = plate;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(80);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (plate){
while(plate.getNum()==0){
try {
plate.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
plate.setNum(plate.getNum()-1);
System.out.println(Thread.currentThread().getName()+"---"+plate.getNum());
plate.notifyAll();
}
}
}
}
public class Produce implements Runnable{
/**
* 临界资源
*/
private Plate plate;
public Produce(Plate plate) {
this.plate = plate;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (plate){
while(plate.getNum()==10){
try {
plate.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
plate.setNum(plate.getNum()+1);
System.out.println(Thread.currentThread().getName()+"---"+plate.getNum());
plate.notifyAll();
}
}
}
}
public class Plate {
private int num=0;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public static void main(String[] args) {
Plate plate=new Plate();
new Thread(new Consumer(plate)).start();
new Thread(new Produce(plate)).start();
new Thread(new Produce(plate)).start();
}
}
3.线程间的通信(wait和notify)
3.1通知等待机制
通知/等待机制的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类Java.lang.Object上
wait();//调用该方法的线程进入WAITING状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用wait()方法后,会释放对象的锁 。
notify();//通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁
notifyAll();//通知所有等待在该对象的线程
**注意:**wait、notify和notyfyAll要和synchronized一起使用,否则会报以下的错
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.mayikt.thread.days02.Thread03.run(Thread03.java:16)
3.2 Join/wait和sleep之间的区别
方法 | 区别 |
---|---|
sleep(long) | 线程在睡眠使不释放对象锁 |
join(long) | join(long)方法先执行另外的一个线程,在等待的过程中释放对象锁 底层是基于wait封装的, |
wait(long) | Wait(long)方法在等待的过程中释放对象锁 需要在我们synchronized中使用 |
3.2.1如何让三个线程T1,T2,T3按顺序执行
Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t1");
Thread t2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t2");
Thread t3 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t3");
t1.start();
t2.start();
t3.start();
Thread t1 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + ",线程执行");
}, "t1");
Thread t2 = new Thread(() -> {
try {
t1.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ",线程执行");
}, "t2");
Thread t3 = new Thread(() -> {
try {
t2.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ",线程执行");
}, "t3");
t1.start();
t2.start();
t3.start();
使用join()可以实现3个线程顺序执行,原因分析:
t1,t2,t3并发执行,而执行t2的异步逻辑代码时遇到了t1.join。本质就是t2线程被 t1线程实例对象作为对象锁进行t1.wait()阻塞掉了,需要等待t1线程执行完成释放掉t1对象锁,t2线程获取t1的对象锁在进行执行
如果对上述描述不太理解,那我们就分析一下join的底层原理
Thread t1 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + ",线程执行");
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (t1){
try {
t1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// try {
// t1.join();
// } catch (InterruptedException e) {
//
// }
System.out.println(Thread.currentThread().getName() + ",线程执行");
}, "t2");
本质t2线程就是被 t1线程对象阻塞掉了即 t1.wait();
这个线程如何被唤醒的?唤醒的代码在jvm Hotspot 源码中 当jvm在关闭t1线程之前会检测阻塞
在t1线程对象上的线程,然后执行notfyAll(),这样t2线程就被唤醒了