博主最近在学习高洪岩编写的《Java多线程编程核心技术》,之前有一篇:
JAVA多线程学习笔记(一): 多线程的基础概念以及Thread类常用方法介绍
这一篇主要整理的是线程同步的方法以及线程间通讯:
文章目录
4 线程同步
4.1 线程不安全
4.1.1 现象&原因
- 用线程访问的对象中如果有多个实例变量,则运行的结果有可能出现交叉的情况
- 一个线程在操作共享数据的过程中,未执行完的情况下,另外的线程参与进来,导致共享数据出现安全问题
4.1.2 如何解决
- 必须让一个线程操作完共享数据之后,才可以让另外的线程进入
- java如何实现线程安全:线程同步
4.1.3 注意点
- 方法内的变量:线程安全。各个线程私有的变量,不会互相访问到
- 实例变量:非线程安全。如果多个线程共同访问1个对象中的实例变量,会出现线程不安全的现象
4.2 synchronized 关键字
4.2.1 简介
synchronized关键字在java用于实现线程同步,只要的方式有两种
- 同步代码块:对象监视器为Object时使用
- 同步方法:对象监视器为Class时使用
4.2.2 同步代码块
将操作同步数据的代码块,用synchronized括号包起来,即括号内为同步的代码块,将保证一个线程访问该部分代码时,其他方法在外等待直至此线程执行完该部分代码块
- 共享数据:多个线程共同操作的同一个对象
- 同步监视器:由 一个类的对象(object) 来充当,哪一个线程取此监视器,谁就可以执行同步代码块的内容。俗称:锁
同步代码块:
synchronized(Object object/*同步监视器*/){
// 需要被同步的代码块(即为操作共享数据的代码)
}
注意点:
- 必须共用同一把锁才能启动作用,当没有办法实现同步的时候,要去判断作为锁的对象是否时唯一的。如果每个线程都创建一个新的对象的,起不到任何作用
- 对于继承thread实现的线程,注意同步监视器的对象是否需要加上static
- 当一个线程访问object的一个synchroniezd同步代码块时,另一个线程仍可以访问该对象中的非synchronized(this)同步代码块
- synchronized(this)代码块锁定的是当前对象
4.2.3 同步方法
将操作同步数据的方法,声明为synchronized,即此方法为同步方法,将保证一个线程访问该方法时,其他方法在外等待直至此线程执行完此方法
// 在需要实现同步的方法加上关键字synchronized
synchronized method(){
}
注意点:
- 同步不具有继承性,还需要在子类的方法中添加synchronized关键字
- 对一个类的方法加上synchronized,加锁的时候,是对对象进行加锁,相当于,两个线程访问同一个对象的两个不同的同步方法,执行的结果是同步的(因此可以一个线程异步执行A对象的同步方法时,另外一个线程执行A对象的非同步方法)
- 对一个static方法加上synchronized,加锁的时候,是给Class类加上锁,相当于实现了synchronized(class)的作用(即,锁定该类的所有实例)
4.3 volatile 关键字
4.3.1 作用
- 主要作用是使得变量在多个线程之间可见,但volatile关键字最致命的缺点就是不支持原子性
4.3.2 原理
- 通过使用volatile关键字,强制的从公共内存中读取变量的值
- 但这里就涉及volatile一个致命的问题,它只保证了强制对数据的读写,但本身不处理数据的原子性
- 举例:i++。非原子操作,分为:①从内存取出i ②计算i的值 ③将i的值写到内存中。如果在第二步出现修改,则会出现脏数据
4.3.3 场景
- 在多个线程中可以感知实例变量被更改了,并且可以获得最新的值的使用,也就是用多线程读取共享变量时可以获得最新值
4.4 关键字synchronized & 关键字 volatile
- volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法、代码块
- 多线程访问volatile不会发生阻塞,synchronized会出现阻塞
- volatile能保证数据的可见性,但不能保证原子性,而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公有内存中的数据做同步(因为synchronized保证在同一时间只有一个线程可以执行某一个方法或某一代码块,这包含了互斥性跟可见性)
- 重点:volatile解决的是变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性
5. 线程间通信
5.1 while语句
- 最原始最直接实现线程之间的通信机制,就是通过while语句来实现轮询机制来检测某一个条件。对应的缺点也很明显:轮询间隔小,则CPU资源的浪费,轮询间隔大,则不一定能得到想要的数据。
5.2 同步机制
- 这里讲的同步是指多个线程通过 synchronized 关键字这种方式来实现线程间的通信
- 这种方法的前提是:多个线程需要访问同一个共享变量。通信的方法:谁拿到了锁(获得了访问权限),谁就可以执行
5.3 等待/通知机制
- 等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。
5.3.1 基础方法
- wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而且当前线程排队等待再次对资源的访问
- notify():随机唤醒正在排队等待同步资源的线程的一个线程结束等待
- notifyAll():唤醒正在排队等待的所有线程结束等待(这里是优先级最高的线程先执行,还是随机执行,取决于JVM的实现)
5.3.2 注意点
- Java.lang.Object提供的这三个方法,只有在synchronized方法或代码块中才能使用
- 方法wait()释放锁,notify()不释放锁。即,线程实行wait()之后,将释放锁,而notify()执行的时候,唤醒其他线程,但并不释放锁。即唤醒的线程不一定马上就可以执行,要等调用notify()方法的线程释放锁粥,等待线程才有机会从wait()返回。
- wait(long)方法的功能是:等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间,则自动唤醒。
5.1.4 经典模型->生产者消费者
- 一生产者一消费者
- 一生产者多消费者
- 多生产者一消费者
- 多生产者多消费者
关于生产者消费者的一个代码样例:
import java.util.LinkedList;
public class Stroge {
// 用于存放生产出来的产品
private LinkedList<String> list;
// 记录最大的容量
private int maxSize;
public Stroge(LinkedList<String> list, int maxSize) {
this.list = list;
this.maxSize = maxSize;
}
// 生产函数
public void produce() {
try {
synchronized (list) {
// 生产者可能被其他生产者唤醒,唤醒之后判断数目,如果无需生产,继续 wait()
while (list.size() == maxSize) {
list.wait();
}
// 生产一个产品
list.add(System.currentTimeMillis() + "");
System.out.println(Thread.currentThread().getName() + " 生产一个产品,当前总数: " + this.list.size());
// 唤醒其他的线程
list.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费产品
public String consume() {
try {
synchronized (list) {
// 消费者可能被其他消费者唤醒,唤醒之后判断数目,没有可消费的,继续 wait()
while (list.size() == 0) {
list.wait();
}
// 取走一个产品
String consume = list.remove();
System.out.println(Thread.currentThread().getName() + " 消费一个产品,当前总数: " + this.list.size());
// 唤醒其他的线程
list.notifyAll();
return consume;
}
} catch (InterruptedException e) {
return null;
}
}
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList<>();
Stroge stroge = new Stroge(linkedList, 10);
// 下面创建两个消费者跟两个生产者
Consumer consumer1 = new Consumer(stroge);
Consumer consumer2 = new Consumer(stroge);
Producer producer1 = new Producer(stroge);
Producer producer2 = new Producer(stroge);
Thread consumerThread1 = new Thread(consumer1);
Thread consumerThread2 = new Thread(consumer2);
Thread produceThread1 = new Thread(producer1);
Thread produceThread2 = new Thread(producer2);
consumerThread1.setName("消费者1");
consumerThread2.setName("消费者2");
produceThread1.setName("生产者1");
produceThread2.setName("生产者2");
consumerThread1.start();
consumerThread2.start();
produceThread1.start();
produceThread2.start();
}
}
class Producer implements Runnable {
private Stroge stroge;
public Producer(Stroge stroge) {
this.stroge = stroge;
}
public void run() {
while (true) {
stroge.produce();
}
}
}
class Consumer implements Runnable {
private Stroge stroge;
public Consumer(Stroge stroge) {
this.stroge = stroge;
}
public void run() {
while (true) {
stroge.consume();
}
}
}
5.4 管道
- 通过管道进行线程间通信:字符流、字符流
5.5 join方法
- 方法 join 的作用是使所属的线程对象 x 正常执行 run() 方法中的任务,而使当前线程 z 进行无限期的阻塞,等待线程 x 销毁后,再继续执行线程 z 后面的代码(即在A线程中调用了B线程的join方法,表示执行到这里,A停止,直到B线程执行完成,再继续执行A线程的剩余代码)
5.6 类ThreadLocal
- ThreadLocal 是多线程中用于解决每个线程自己的共享变量的问题。可以理解为存储每个线程的私有数据