一、线程同步(重点)
1.1 线程安全问题
多个线程执行的不确定性引起执行结果的不稳定;
多线程并发操作同一数据时,会造成操作的不完整性,会破坏数据;
判断是否有线程安全性的一个原则是:是否有多线程访问可变的共享变量;
经典例子:火车票售票案例;
买票肯定是多个窗口同时进行的,那么肯定需要使用多线程;那么我们就尝试使用目前我们学过的多线程技术来实现一下看看效果:
class Ticket implements Runnable {
//1.定义一个总票数
private int tick = 100;
@Override
public void run() {
while (true) {
if (tick > 0) {
//为了效果更明显一点,我们让其每次休眠一会
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出车票,tick号为:" + tick--);
} else {
break;
}
}
}
}
/*
* 使用多线程实现火车票售票
*/
public class TicketTest {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("t1窗口");
t2.setName("t2窗口");
t3.setName("t3窗口");
t1.start();
t2.start();
t3.start();
}
}
经过测试,我们会发现问题,数据紊乱,多线程出现了安全问题。
问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
使用同步技术可以解决这种问题,把操作数据的代码进行同步,不要多个线程一起操作;
1.2 线程同步实现
现实生活中,我们经常会遇到“同一个资源,多人都想使用”的问题,比如,食堂排队打饭,每个人都想吃饭,最好的解决方法就是:排队,一个个来;
处理多线程问题时,多个线程同时访问一个对象,并且某些线程还想修改这个对象,这个时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下个线程在使用。
仅仅只是排队就可以解决问题吗?当然不行,还必须需要锁,每个对象都有一把锁;队列 + 锁 就是用来保证多线程安全性;
1.2.1 三大不安全案例
案例一:火车站售票
public class UnsafeBuyTicket {
public static void main(String[] args) {
// 构建一个Ticket对象
Ticket station = new Ticket();
// 构造多条线程同时买票
new Thread(station,"普通用户").start();
new Thread(station,"合作机构").start();
new Thread(station,"黄牛党").start();
}
}
class Ticket implements Runnable{
//1.定义一个总票数
private int ticketCount = 10;
// 结束循环的标记
boolean isFlag = true;
/*
* 我们通过线程体来实现买票的操作,线程体里面执行的是买票操作 (只要有票,我们都可以去买票)
* 线程体就相当于我们的售票窗口,这个售票窗口难道只能买一张票,肯定可以买多张票
* run里面应该是只要有票,就可以卖;
*/
@Override
public void run() {
//这个窗口可以一直买票
while(isFlag) {
buy();
}
}
/*
* 买票的方法
* */
private void buy(){
// 如果没有票,则结束买票操作,反馈
if(ticketCount <= 0) {
isFlag = false;
return;
}
// 为了效果更明显一点,我们让其每次休眠一会
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---卖出去了第"+ticketCount--+"张票");
}
}
案例二:银行取款
public class UnsafeBank {
public static void main(String[] args) {
// 有一个共享账户
Account account = new Account(1001,10000,"购房存款");
// 创建一个取款线程
Drawing drawing1 = new Drawing("我",account,9000);
drawing1.start();
Drawing drawing2 = new Drawing("小美",account,5000);
drawing2.start();
}
}
class Account{
// 账户id
private int id;
// 账户余额
private double money;
// 钱的用途
private String desc;
public Account(){}
public Account(int id, double money, String desc) {
this.id = id;
this.money = money;
this.desc = desc;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
/*
* 取款线程类
* */
class Drawing extends Thread{
// 取款的账户
private Account account;
// 取款的额度
private double drawMoney;
public Drawing(){}
/*
* 创建线程的时候,定义线程名称和操作的账户以及取款额度
* */
public Drawing(String name, Account account, double drawMoney) {
super(name);
this.account = account;
this.drawMoney = drawMoney;
}
// 取款的业务逻辑
@Override
public void run() {
// 如果取款额度大于卡内余额
if(drawMoney > account.getMoney()){
System.out.println("余额不足!");
return;
}
// 模拟取款过程
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(Thread.currentThread().getName() + ":取了" + drawMoney);
// 更改原账户的额度
account.setMoney(account.getMoney() - drawMoney);
System.out.println("目前卡内余额:" + account.getMoney());
}
}
案例三:ArrayList存储数据
import java.util.ArrayList;
import java.util.List;
public class UnsafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for(int i = 0;i < 10000;i++){
new Thread(){
@Override
public void run() {
/// 因为ArrayList是线程不安全的,可能存在数据覆盖问题
list.add(Thread.currentThread().getName());
}
}.start();
}
Thread.sleep(2000);
System.out.println("集合里面的数据数量:"+list.size());
}
}
1.2.2 synchronized的使用方法
Java对于多线程的安全问题提供了专业的解决方式:同步机制;
由于我们可以通过private关键字来保证数据对象只能被方法访问到,所以我们只需要针对方法提出一套机制,这套机制就是synchronized 关键字,它包含两种用法:
1、同步代码块:
synchronized (同步监视器){
// 需要被同步的代码
}
说明:
操作共享数据的代码,即为需要被同步的代码。
共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
2、synchronized还可以放在方法声明中,表示整个方法为同步方法。
public synchronized void show (String name){
…
}
同步原理:
同步机制中的锁:
在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized 关键字是实现线程同步的关键;
synchronized的锁是什么?
任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class)、非静态方法(this)。
同步代码块:自己指定,很多时候也是指定为this或类名.class。
存在如下问题:
一个线程持有锁会导致其他所有需要此锁的线程挂起;
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题;
如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题;
注意:
必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全;
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎);
- synchronized的使用方法一:同步代码块
1、什么情况下需要同步
当多线程并发,有多段代码同时执行时,我们希望某一段代码执行的过程中CPU不要切换到其他线程工作。这时就需要同步。
如果两段代码是同步的,那么同一时间只能执行一段,在一段代码没执行结束之前,不会执行另外一段代码。
2、同步代码块
使用synchronized关键字加上一个锁对象来定义一段代码,这就叫同步代码块,多个同步代码块如果使用相同的锁对象,那么他们就是同步的
synchronized(Obj){
}
Obj称之为同步监视器;Obj可以是任何对象,但是推荐使用共享资源作为同步监视器;同步方法无需指明同步监视器,因为同步方法的同步监视器就是this;
3、同步监视器的执行过程:
第一个线程访问,锁定同步监视器,执行其中代码;
第二个线程访问,发现同步监视器锁定,无法访问;
第一个线程访问完毕,释放同步监视器;
第二个线程访问,发现同步监视器没有锁,然后锁定访问;
注:银行的例子同步方法无法实现,因为共享资源是账户,只能使用同步代码块;
线程安全的火车站售票:
public class Window1 extends Thread{
// 定义票数,如果没有添加static,那么表示每个线程有100张票
private static int ticket = 100;
private static Object obj = new Object();
// run()定义买票的业务逻辑
@Override
public void run() {
// 只要有票,都可以卖
while(true){
// synchronized (this){ // 错误: this分别是w1、w2、w3
// synchronized (obj){ // 静态成员obj可以,但是成员变量obj不行
synchronized (Window1.class){ // 类对象也可以
if(ticket > 0){
// 模拟卖票的过程,是需要耗时的
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖出了票,票号是: " + ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class Window2 implements Runnable{
// 定义票数,如果实现的是Runnable接口,是不是静态的就无所谓了
private int ticket = 100;
Object obj = new Object();
Person person = new Person();
// run()定义买票的业务逻辑
@Override
public void run() {
// 只要有票,都可以卖
while(true){
// 当我们把需要同步代码放在代码块中,并使用synchronized修饰,那么线程来访问这段代码的时候,就需要排队 ->队列
// 我们需要定义一个同步监视器(锁),任意对象都可以作为同步监视器
// 多个线程来访问同步代码块的时候,必须访问的是通一把锁;
// 正确的
// synchronized(this){
// synchronized(obj){
// synchronized(person){
// 错误的
// synchronized(new Person()){
synchronized(Window2.class){ // 类名.属性
if(ticket > 0){
// 模拟卖票的过程,是需要耗时的
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖出了票,票号是: " + ticket);
ticket--;
}else{
break;
}
}
}
}
}
class Person{
}
public class WindowTest {
public static void main(String[] args) {
// 创建3个卖票的线程
Window1 w1 = new Window1();
Window1 w2 = new Window1();
Window1 w3 = new Window1();
// 启动线程
w1.start();
w2.start();
w3.start();
// 创建3个卖票的线程
/* Window2 w2 = new Window2();
Thread t1 = new Thread(w2);
Thread t2 = new Thread(w2);
Thread t3 = new Thread(w2);
// 启动线程
t1.start();
t2.start();
t3.start();*/
}
}
线程安全的银行取款:
public class SafeBank {
public static void main(String[] args) {
// 有一个共享账户
Account account = new Account(1001,10000,"购房存款");
// 创建一个取款线程
Drawing drawing1 = new Drawing("我",account,9000);
drawing1.start();
Drawing drawing2 = new Drawing("小美",account,5000);
drawing2.start();
}
}
class Account{
// 账户id
private int id;
// 账户余额
private double money;
// 钱的用途
private String desc;
public Account(){}
public Account(int id, double money, String desc) {
this.id = id;
this.money = money;
this.desc = desc;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
/*
* 取款线程类
* */
class Drawing extends Thread{
// 取款的账户
private Account account;
// 取款的额度
private double drawMoney;
public Drawing(){}
/*
* 创建线程的时候,定义线程名称和操作的账户以及取款额度
* */
public Drawing(String name, Account account, double drawMoney) {
super(name);
this.account = account;
this.drawMoney = drawMoney;
}
// 取款的业务逻辑
@Override
public void run() {
// 错误的
// synchronized (this){
// 正确的
// synchronized (Drawing.class){
// account是成员变量
synchronized (account){
// 如果取款额度大于卡内余额
if(drawMoney > account.getMoney()){
System.out.println("余额不足!");
return;
}
// 模拟取款过程
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(Thread.currentThread().getName() + ":取了" + drawMoney);
// 更改原账户的额度
account.setMoney(account.getMoney() - drawMoney);
System.out.println("目前卡内余额:" + account.getMoney());
}
}
}
线程安全的ArrayList存储数据:
import java.util.ArrayList;
import java.util.List;
public class SafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for(int i = 0;i < 10000;i++){
new Thread(){
@Override
public void run() {
synchronized (list){
// 锁住list,当线程向集合中添加数据的时候,其他线程等待;不会出现问题
list.add(Thread.currentThread().getName());
}
}
}.start();
}
Thread.sleep(2000);
System.out.println("集合里面的数据数量:"+list.size());
}
}
- synchronized的使用方法二:同步方法
使用synchronized关键字修饰一个方法, 该方法中所有的代码都是同步的。
对共享资源进行操作的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。
多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推...
缺点:若将一个大的方法声明为synchronized将会影响效率;
实现Runnable接口的线程安全的火车站售票案例:
public class Window2 implements Runnable{
// 定义票数,如果实现的是Runnable接口,是不是静态的就无所谓了
private int ticket = 100;
// 定义标志位
private boolean isFlag = true;
// run()定义买票的业务逻辑
@Override
public void run() {
// 只要有票,都可以卖
while(isFlag){
buy();
}
}
private synchronized void buy(){ // 谁来调用该方法,那么锁的就是谁,相当于this
if(ticket > 0){
// 模拟卖票的过程,是需要耗时的
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖出了票,票号是: " + ticket);
ticket--;
}else{
isFlag = false;
return;
}
}
}
- synchronized的使用方法三:静态同步方法
格式:将同步加在静态方法上。此时的锁对象为当前类的字节码文件对象。
为了更好的理解此时的锁对象,通过下边的代码来直观的理解只有同一锁对象时才能解决线程安全问题:
public class Window1 extends Thread{
// 定义票数,如果没有添加static,那么表示每个线程有100张票
private static int ticket = 100;
private static boolean isFlag = true;
// run()定义买票的业务逻辑
@Override
public void run() {
// 只要有票,都可以卖
while(isFlag){
buy();
}
}
// 修饰静态方法,锁的是Window1.class
private static synchronized void buy(){
if(ticket > 0){
// 模拟卖票的过程,是需要耗时的
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖出了票,票号是: " + ticket);
ticket--;
}else{
isFlag = false;
return;
}
}
}
- 同步的注意事项
同步的范围:
1、如何找问题,即代码是否存在线程安全?(非常重要)
(1)明确哪些代码是多线程运行的代码
(2)明确多个线程是否有共享数据
(3)明确多线程运行代码中是否有多条语句操作共享数据
2、如何解决呢?(非常重要)
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
即所有操作共享数据的这些语句都要放在同步范围中。
3、切记:
范围太小:没锁住所有有安全问题的代码。
范围太大:没发挥多线程的功能。
释放锁的操作:
当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作:
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
应尽量避免使用suspend()和resume()来控制线程。
1.2.3 Lock锁
从JDK5开始,Java提供了更强大的线程安全机制:通过显式定义同步锁对象来实现同步,同步锁使用Lock及其子类实现;
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获取Lock对象。
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的就是ReentrantLock,可以显式加锁和释放锁;
将需要一次执行完代码块用lock()和unlock()包裹,来保证线程安全。
注:CopyOnWriteArrayList底层就是使用了ReentrantLock;
使用方式:
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}
finally{
lock.unlock();
}
}
}
注意:如果同步代码有异常,要将unlock()写入finally语句块。
public class Window1 extends Thread{
// 定义票数,如果没有添加static,那么表示每个线程有100张票
private static int ticket = 100;
private static final ReentrantLock lock = new ReentrantLock();
// run()定义买票的业务逻辑
@Override
public void run() {
// 只要有票,都可以卖
while(true){
lock.lock();
try {
if(ticket > 0){
// 模拟卖票的过程,是需要耗时的
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖出了票,票号是: " + ticket);
ticket--;
}else{
break;
}
}finally {
lock.unlock();
}
}
}
}
public class Window2 implements Runnable{
// 定义票数,如果实现的是Runnable接口,是不是静态的就无所谓了
private int ticket = 100;
// 创建一个锁对象
private final ReentrantLock lock = new ReentrantLock();
// run()定义买票的业务逻辑
@Override
public void run() {
// 只要有票,都可以卖
while(true){
// 在同步代码之前加锁
lock.lock();
try{
if(ticket > 0){
// 模拟卖票的过程,是需要耗时的
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖出了票,票号是: " + ticket);
ticket--;
}else{
break;
}
}finally {
lock.unlock();
}
}
}
}
synchronized和Lock的对比:
1、Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放;
2、Lock只有代码块锁,synchronized有代码块锁和方法锁;
3、使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,而且具有更好的扩展性(提供更多子类);
4、优先使用顺序:
Lock -> 同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法(在方法体之外)
1.2.4 锁的分类
方法锁(synchronized修饰方法时):
通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。
synchronized 方法控制对类成员变量的访问;每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。
对象锁(synchronized修饰方法或代码块):
当一个对象中有synchronized method或synchronized block的时候调用此对象的同步方法或进入其同步区域时,就必须先获得对象锁。如果此对象的对象锁已被其他调用者占用,则需要等待此锁被释放。(方法锁也是对象锁)
java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
对象锁的两种形式:同步方法、同步代码块;
public class Test {
// 对象锁:形式1(方法锁)
public synchronized void Method1() {
System.out.println("我是对象锁也是方法锁");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 对象锁:形式2(代码块形式)
public void Method2() {
synchronized (this) {
System.out.println("我是对象锁");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
类锁(synchronized 修饰静态的方法或代码块):
由于一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。所以,一旦一个静态的方法被申明为synchronized。此类所有的实例化对象在调用此方法,共用同一把锁,我们称之为类锁。
对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。
类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。
java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是[类名.class]的方式。
1.2.5 线程安全集合类测试
JUC就是java.util.concurrent工具包的简称。这是一个处理线程的工具包,JDK 1.5开始出现的。
CopyOnWriteArrayList是一个线程安全的变体ArrayList ,其中所有可变操作( add , set ,等等)通过对底层数组的最新副本实现。
import java.util.concurrent.CopyOnWriteArrayList;
public class SafeList {
public static void main(String[] args) throws InterruptedException {
// 线程安全的ArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread() {
@Override
public void run() {
list.add(Thread.currentThread().getName());
}
}.start();
}
Thread.sleep(2000);
System.out.println("集合里面的数据数量:" + list.size());
}
}
1.3 死锁
多个线程各自占用一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或多个线程都等待对方释放资源,都停止执行的情况,某一个同步块同时拥有“两个以上对象的锁”时,就可能会出现“死锁”问题。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决方法:
专门的算法、原则;
尽量减少同步资源的定义;
尽量避免嵌套同步;
public class DeadLock {
private static String s1 = "筷子左";
private static String s2 = "筷子右";
public static void main(String[] args) {
new Thread() {
public void run() {
while (true) {
synchronized (s1) {
System.out.println(getName() + "...获取" + s1 + "等待" + s2);
synchronized (s2) {
System.out.println(getName() + "...拿到" + s2 + "开吃");
}
}
}
}
}.start();
new Thread() {
public void run() {
while (true) {
synchronized (s2) {
System.out.println(getName() + "...获取" + s2 + "等待" + s1);
synchronized (s1) {
System.out.println(getName() + "...拿到" + s1 + "开吃");
}
}
}
}
}.start();
}
}
练习题:
银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
【提示】
1,明确哪些代码是多线程运行代码,须写入run()方法
2,明确什么是共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。
拓展问题:可否实现两个储户交替存钱的操作
class Account{
private double balance;
public Account(double balance) {
this.balance = balance;
}
//存钱
public synchronized void deposit(double amt){
if(amt > 0){
balance += amt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":存钱成功。余额为:" + balance);
}
}
}
class Customer extends Thread{
private Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account acct = new Account(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
二、线程通信
2.1 线程通信概述
2.1.1 理解线程通信
线程是操作系统调度的最小单位,有自己的栈空间,可以按照既定的代码逐步的执行,但是如果每个线程间都孤立的运行,那就会造成资源浪费。所以在现实中,我们需要这些线程间可以按照指定的规则共同完成一件任务,所以这些线程之间就需要互相协调,这个过程被称为线程的通信。
常规情况下,如果多个线程做的事情没有关联,我们的线程都是可以并发执行的;但是在有些情况下,线程之间是存在一定的交互的,那么这个交互怎么实现? 就需要通信。
举例:
比如我们现在有三个生产线,一个生产可口可乐的瓶子,一个灌装饮料,一个生产瓶盖;
这三个生产线是可以并发执行的,当然是有前提的;因为得到一个成品可乐需要瓶子、饮料、瓶盖这三部分共同参与;
我们生产的瓶子需要地方存储,如果存储瓶子的地方没有满,那么就可以一直生产,但是如果满了呢?那么该线程肯定停止工作,等待有空余的存储空间才可以继续生产;
我们灌装饮料,如果有瓶子就可以一直进行,但是如果瓶子没了,那么要通知生产瓶子的生产线去工作了;
瓶盖类似...
线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作。
通信的目的是为了更好的协作,线程无论是交替式执行,还是接力式执行,都需要使线程间能够互相发送信号。例如,线程B可以等待线程A的一个信号,这个信号会通知线程B数据已经准备好了。
比如说最经典的 生产者-消费者模型:
生产者消费者模式并不是GOF提出的23种设计模式之一,23种设计模式都是建立在面向对象的基础之上的,但其实面向过程的编程中也有很多高效的编程模式,生产者消费者模式便是其中之一,它是我们编程过程中最常用的一种设计模式。
在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。
大概的结构如下图:
为了不至于太抽象,我们举一个寄信的例子(虽说这年头寄信已经不时兴,但这个例子还是比较贴切的)。假设你要寄一封信,大致过程如下:
1、你把信写好——相当于生产者制造数据
2、你把信放入邮筒——相当于生产者把数据放入缓冲区
3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区
4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据
当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。
因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。
缓冲区的作用:
可能有同学会问了:这个缓冲区有什么用捏?为什么不让生产者直接调用消费者的某个函数,直接把数据传递过去?搞出这么一个缓冲区作甚?其实这里面是大有讲究的,大概有如下一些好处。
1、解耦:
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
接着上述的例子,如果不使用邮筒(也就是缓冲区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。
2、支持并发(concurrency):
生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。
使用了生产者/消费者模式之后,生产者和消费者可以是两个独立的并发主体(常见并发类型有进程和线程两种)。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据。基本上不用依赖消费者的处理速度。
其实当初这个模式,主要就是用来处理并发问题的。
从寄信的例子来看。如果没有邮筒,你得拿着信傻站在路口等邮递员过来收(相当于生产者阻塞);又或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。不管是哪种方法,都挺土的。
3、支持忙闲不均:
缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
为了充分复用,我们再拿寄信的例子来说事。假设邮递员一次只能带走1000封信。万一某次碰上情人节(也可能是圣诞节)送贺卡,需要寄出去的信超过1000封,这时候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来时再拿走。
2.1.2 Java线程的通信方式
线程通信主要可以分为三种方式,分别为共享内存、消息传递和管道流。每种方式有不同的方法来实现:
共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。
volatile共享内存
消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。
等待/通知机制、join方式、Condition
管道流:管道输入/输出流的形式。
题目:有两个线程A、B,A线程向一个集合里面依次添加元素"abc"字符串,一共添加十次,当添加到第五次的时候,希望B线程能够收到A线程的通知,然后B线程执行相关的业务操作。
2.2 利用volatile通信
2.2.1 volatile概述及原理
在学习Volatile之前,我们先了解下Java的内存模型:
在Java中,所有堆内存中的所有的数据(实例域、静态域和数组元素)存放在主内存中可以在线程之间共享,一些局部变量、方法中定义的参数存放在本地内存中不会在线程间共享。线程之间的共享变量存储在主内存中,本地内存存储了共享变量的副本。
如果线程A要和线程B通信,则需要经过以下步骤:
1、线程A把本地内存A更新过的共享变量刷新到主内存中。
2、线程B到内存中去读取线程A之前已更新过的共享变量。
这保证了线程间的通信必须经过主内存,下面引出我们要学习的关键字volatile。
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。
volatile语义保证线程可见性,有两个原则保证:
所有volatile修饰的变量一旦被某个线程更改,必须立即刷新到主内存。
所有volatile修饰的变量在使用之前必须重新读取主内存的值。
volatile修饰的变量值直接存在主内存里面,子线程对该变量的读写直接写住内存,而不是像其它变量一样在local thread里面产生一份copy。volatile能保证所修饰的变量对于多个线程可见性,即只要被修改,其它线程读到的一定是最新的值。
volatile保证可见性原理图:
工作内存2能够感知到工作内存1更新a值是靠的总线,工作内存1在将值刷新的主内存时必须经过总线,总线就能告知其他线程有值被改变,那么其他线程就会主动读取主内存的值来更新。
2.2.2 volatile示例代码
import java.util.ArrayList;
import java.util.List;
public class TestSync {
// 定义一个共享变量来实现通信,它需要是volatile修饰,否则线程不能及时感知
static volatile boolean notice = false;
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
notice = true;
}
});
// 实现线程B
Thread threadB = new Thread(() -> {
while (true) {
if (notice) {
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
}
});
// 需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}
执行结果:
线程A向list中添加一个元素,此时list中的元素个数为:1
线程A向list中添加一个元素,此时list中的元素个数为:2
线程A向list中添加一个元素,此时list中的元素个数为:3
线程A向list中添加一个元素,此时list中的元素个数为:4
线程A向list中添加一个元素,此时list中的元素个数为:5
线程A向list中添加一个元素,此时list中的元素个数为:6
线程B收到通知,开始执行自己的业务...
线程A向list中添加一个元素,此时list中的元素个数为:7
线程A向list中添加一个元素,此时list中的元素个数为:8
线程A向list中添加一个元素,此时list中的元素个数为:9
线程A向list中添加一个元素,此时list中的元素个数为:10
代码分离:
public class ThreadA extends Thread {
private List<String> list = new ArrayList<>();
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
Data.notice = true;
}
}
}
public class ThreadB extends Thread {
@Override
public void run() {
while(true){
if (Data.notice){
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
}
}
}
public class Data {
// 定义一个共享变量来实现通信,它需要是volatile修饰,否则线程不能及时感知
static volatile boolean notice = false;
}
public class TestSync {
public static void main(String[] args) {
// 需要先启动线程B
ThreadB threadB = new ThreadB();
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
ThreadA threadA = new ThreadA();
threadA.start();
}
}
2.3 等待/通知机制(重点)
众所周知,Object类提供了线程间通信的方法:wait()、notify()、notifyAll(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。
等待通知机制是基于wait和notify方法来实现的,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被通知或者被唤醒。
注意:wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁;
2.3.1 方法讲解
等待/通知机制 主要由 Object类 中的以下三个方法保证:
wait()、notify() 和 notifyAll()
上述三个方法均非Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有monitor(锁),所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作,而不是用当前线程来操作,因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。
注意:
1、wait()、notify() 和 notifyAll()这三个方法必须使用在同步代码块或同步方法中;
2、wait()、notify() 和 notifyAll()这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则抛出异常;
3、wait()、notify() 和 notifyAll()这三个方法是定义在Object类中;
2.3.2 方法调用与线程状态关系
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度;反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。
2.3.3 等待/通知机制示例代码
示例代码1
public class Test {
public static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.start();
Thread.sleep(2000);
thread2.start();
}
static class Thread1 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁...");
try {
System.out.println("线程" + Thread.currentThread().getName() + "阻塞并释放锁...");
object.wait();
} catch (InterruptedException e) {
}
System.out.println("线程" + Thread.currentThread().getName() + "执行完成...");
}
}
}
static class Thread2 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁...");
object.notify();
System.out.println("线程" + Thread.currentThread().getName() + "唤醒了正在wait的线程...");
}
System.out.println("线程" + Thread.currentThread().getName() + "执行完成...");
}
}
}
测试结果:
线程Thread-0获取到了锁...
线程Thread-0阻塞并释放锁...
线程Thread-1获取到了锁...
线程Thread-1唤醒了正在wait的线程...
线程Thread-1执行完成...
线程Thread-0执行完成...
示例代码2
import java.util.ArrayList;
import java.util.List;
public class TestSync {
public static void main(String[] args) {
// 定义一个锁对象
Object lock = new Object();
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
synchronized (lock) {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
lock.notify();// 唤醒B线程
}
}
});
// 实现线程B
Thread threadB = new Thread(() -> {
while (true) {
synchronized (lock) {
if (list.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
}
});
// 需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}
执行结果:
线程A向list中添加一个元素,此时list中的元素个数为:1
线程A向list中添加一个元素,此时list中的元素个数为:2
线程A向list中添加一个元素,此时list中的元素个数为:3
线程A向list中添加一个元素,此时list中的元素个数为:4
线程A向list中添加一个元素,此时list中的元素个数为:5
线程A向list中添加一个元素,此时list中的元素个数为:6
线程A向list中添加一个元素,此时list中的元素个数为:7
线程A向list中添加一个元素,此时list中的元素个数为:8
线程A向list中添加一个元素,此时list中的元素个数为:9
线程A向list中添加一个元素,此时list中的元素个数为:10
线程B收到通知,开始执行自己的业务...
由打印结果截图可知,在线程A发出notify()唤醒通知之后,依然是走完了自己线程的业务之后,线程B才开始执行,这也正好说明了,notify()方法不释放锁,而wait()方法释放锁。
wait流程原理图
代码分离:
public class Data {
// 定义一个锁对象
static Object lock = new Object();
// 定义集合存储数据
static List<String> list = new ArrayList<>();
}
public class ThreadA extends Thread {
@Override
public void run() {
synchronized (Data.lock) {
for (int i = 1; i <= 10; i++) {
Data.list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + Data.list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (Data.list.size() == 5)
Data.lock.notify();// 唤醒B线程
}
}
}
}
public class ThreadB extends Thread {
@Override
public void run() {
while (true) {
synchronized (Data.lock) {
if (Data.list.size() != 5) {
try {
Data.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
}
}
}
public class TestSync {
public static void main(String[] args) {
// 需要先启动线程B
ThreadB threadB = new ThreadB();
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
ThreadA threadA = new ThreadA();
threadA.start();
}
}
示例代码3
// 使用两个线程打印 1-100。线程1, 线程2 交替打印
class Number implements Runnable{
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj) {
obj.notify();
if(number <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
2.3.4 多个同类型线程的场景(wait 的条件发生变化)
import java.util.ArrayList;
import java.util.List;
public class Run {
public static void main(String[] args) throws InterruptedException {
// 锁对象
String lock = new String("");
ThreadSubtract subtract1Thread = new ThreadSubtract(lock, "subtract1Thread");
subtract1Thread.start();
ThreadSubtract subtract2Thread = new ThreadSubtract(lock, "subtract2Thread");
subtract2Thread.start();
Thread.sleep(1000);
ThreadAdd addThread = new ThreadAdd(lock, "addThread");
addThread.start();
}
}
// 资源类
class ValueObject {
public static List<String> list = new ArrayList<String>();
}
// 元素添加线程
class ThreadAdd extends Thread {
private String lock;
public ThreadAdd(String lock, String name) {
super(name);
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
ValueObject.list.add("anyString");
lock.notifyAll(); // 唤醒所有 wait 线程
}
}
}
//元素删除线程
class ThreadSubtract extends Thread {
private String lock;
public ThreadSubtract(String lock, String name) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
if (ValueObject.list.size() == 0) {
System.out.println("wait begin ThreadName=" + Thread.currentThread().getName());
lock.wait();
System.out.println("wait end ThreadName=" + Thread.currentThread().getName());
}
ValueObject.list.remove(0);
System.out.println("list size=" + ValueObject.list.size());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
wait begin ThreadName=subtract1Thread
wait begin ThreadName=subtract2Thread
wait end ThreadName=subtract2Thread
list size=0
wait end ThreadName=subtract1Thread
Exception in thread "subtract1Thread" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.remove(ArrayList.java:496)
at demo01.ThreadSubtract.run(Run.java:70)
当线程subtract1Thread 被唤醒后,将从 wait处 继续执行。但由于线程subtract2Thread 先获取到锁得到运行,导致 线程subtract1Thread 的 wait的条件发生变化(不再满足),而线程subtract1Thread 却毫无所知,导致异常产生。
像这种有多个相同类型的线程场景,为防止wait的条件发生变化而导致的线程异常终止,我们在阻塞线程被唤醒的同时还必须对wait的条件进行额外的检查,如下所示:
class ThreadSubtract extends Thread {
private String lock;
public ThreadSubtract(String lock, String name) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
while (ValueObject.list.size() == 0) { // 将 if 改成 while
System.out.println("wait begin ThreadName=" + Thread.currentThread().getName());
lock.wait();
System.out.println("wait end ThreadName=" + Thread.currentThread().getName());
}
ValueObject.list.remove(0);
System.out.println("list size=" + ValueObject.list.size());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.3.5 sleep() 和 wait()的异同
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
1、两个方法声明的位置不同:Thread类中声明sleep(), Object类中声明wait()。
2、调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中。
3、关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
2.4 Condition(了解)
2.4.1 API介绍
Condition是在Java 1.5中出现的,它用来替代传统的Object的wait()/notify()/notifyAll实现线程间的协作,它的使用依赖于 Lock;
相比使用Object的wait()/notify(),使用Condition的await()/signal()/signalAll()这种方式能够更加安全和高效地实现线程间协作。
Condition 是个接口,基本的方法就是 await()和signal()方法。Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。
必须要注意的是,Condition 的 await()/signal() 使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock之间才可以使用。
事实上,Conditon的await()/signal() 与 Object的wait()/notify() 有着天然的对应关系:
Conditon中的await() 对应 Object的wait();
Condition中的signal() 对应 Object的notify();
Condition中的signalAll() 对应 Object的notifyAll()。
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
使用Condition往往比使用传统的通知等待机制(Object的wait()/notify())要更灵活、高效,例如,我们可以使用多个Condition实现通知部分线程:
JDK1.5的新特性互斥锁:
1、同步
使用ReentrantLock类的lock()和unlock()方法进行同步。
ReentrantLock一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
2、通信
使用ReentrantLock类的newCondition()方法可以获取Condition对象。
需要等待的时候使用Condition的await()方法, 唤醒的时候用signal()方法。
不同的线程使用不同的Condition, 这样就能区分唤醒的时候找哪个线程了。
2.4.2 Condition示例代码
示例代码1
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TestSync {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
lock.lock();
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
condition.signal();
}
lock.unlock();
});
// 实现线程B
Thread threadB = new Thread(() -> {
lock.lock();
if (list.size() != 5) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
lock.unlock();
});
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadA.start();
}
}
执行结果:
线程A向list中添加一个元素,此时list中的元素个数为:1
线程A向list中添加一个元素,此时list中的元素个数为:2
线程A向list中添加一个元素,此时list中的元素个数为:3
线程A向list中添加一个元素,此时list中的元素个数为:4
线程A向list中添加一个元素,此时list中的元素个数为:5
线程A向list中添加一个元素,此时list中的元素个数为:6
线程A向list中添加一个元素,此时list中的元素个数为:7
线程A向list中添加一个元素,此时list中的元素个数为:8
线程A向list中添加一个元素,此时list中的元素个数为:9
线程A向list中添加一个元素,此时list中的元素个数为:10
线程B收到通知,开始执行自己的业务...
线程B在被A唤醒之后由于没有获取锁还是不能立即执行,也就是说,A在唤醒操作之后,并不释放锁。这种方法跟Object的 wait()和 notify() 一样。
示例代码2
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Run {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
Thread.sleep(3000);
service.signalAll_A();
}
}
//线程 A
class ThreadA extends Thread {
private MyService service;
public ThreadA(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.awaitA();
}
}
//线程 B
class ThreadB extends Thread {
private MyService service;
public ThreadB(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.awaitB();
}
}
class MyService {
private Lock lock = new ReentrantLock();
// 使用多个Condition实现通知部分线程
public Condition conditionA = lock.newCondition();
public Condition conditionB = lock.newCondition();
public void awaitA() {
lock.lock();
try {
System.out.println(
"begin awaitA时间为" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
conditionA.await();
System.out.println(
"end awaitA时间为" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void awaitB() {
lock.lock();
try {
System.out.println(
"begin awaitB时间为" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
conditionB.await();
System.out.println(
"end awaitB时间为" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signalAll_A() {
try {
lock.lock();
System.out.println(
"signalAll_A时间为" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
conditionA.signalAll();
} finally {
lock.unlock();
}
}
public void signalAll_B() {
try {
lock.lock();
System.out.println(" signalAll_B时间为" + System.currentTimeMillis() + " ThreadName="
+ Thread.currentThread().getName());
conditionB.signalAll();
} finally {
lock.unlock();
}
}
}
输出结果如下图所示,我们可以看到只有线程A被唤醒,线程B仍然阻塞。
begin awaitA时间为1640618252157 ThreadName=A
begin awaitB时间为1640618252160 ThreadName=B
signalAll_A时间为1640618255156 ThreadName=main
end awaitA时间为1640618255157 ThreadName=A
实际上,Condition 实现了一种分组机制,将所有对临界资源进行访问的线程进行分组,以便实现线程间更精细化的协作,例如通知部分线程。我们可以从上面例子的输出结果看出,只有conditionA范围内的线程A被唤醒,而conditionB范围内的线程B仍然阻塞。
三、面试题
1、什么线程安全问题(*)?
多线程并发操作同一数据时,会造成操作的不完整性,会破坏数据;
问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
多个线程操作同一数据,如果当前线程a没有操作完该数据,其他线程参与进来执行,那么就会导致数据的错误,这就是线程安全问题;
一定要明确的是:同一数据
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
使用同步技术可以解决这种问题, 把操作数据的代码进行同步, 不要多个线程一起操作;
2、什么是线程同步?
线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下个线程在使用。
每个对象都有一把锁;队列 + 锁 就是用来保证多线程安全性;
3、解决线程安全的方法(***)?
synchronized代码块:使用synchronized关键字加上一个锁对象来定义一段代码, 这就叫同步代码块;
synchronized方法: 使用synchronized关键字修饰一个方法, 该方法中所有的代码都是同步的;
Lock锁: 从JDK5开始,Java提供了更强大的线程安全机制 - 通过显式定义同步锁对象来实现同步,同步锁使用Lock及其子类实现;
4、释放锁的操作(***)?
当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
5、synchronized和Lock的对比(***)?
1、Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放;
2、Lock只有代码块锁,synchronized有代码块锁和方法锁;
3、使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,而且具有更好的扩展性(提供更多子类);
4、优先使用顺序:
Lock -> 同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法(在方法体之外)
6、什么是死锁(*)?
多个线程各自占用一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或多个线程都等待对方释放资源,都停止执行的情况,某一个同步块同时拥有“两个以上对象的锁”时,就可能会出现“死锁”问题;
7、理解线程通信?
常规情况下,如果多个线程做的事情没有关联,我们的线程都是可以并发执行的;
但是在有些情况下,线程之间是存在一定的交互的,那么这个交互怎么实现? 就需要通信
举例:
比如我们现在有三个生产线,一个生产可口可乐的瓶子,一个灌装饮料,一个生产瓶盖;
这三个生产线是可以并发执行的,当然是有前提的; 因为得到一个成品可乐需要瓶子、饮料、瓶盖这三部分共同参与;
我们生产的瓶子可以需要地方存储,如果存储瓶子的地方没有满,那么就可以一直生产,但是如果满了呢?那么该线程肯定停止工作,等待有空余的存储空间才可以继续生产;
我们灌装饮料,如果有瓶子就可以一直进行,但是如果瓶子没了,那么要通知生产瓶子的生产线去工作了;
瓶盖类似...
概念:如果需要这些线程间可以按照指定的规则共同完成一件任务,所以这些线程之间就需要互相协调,这个过程被称为线程的通信。
目的:为了更好的协作,线程无论是交替式执行,还是接力式执行,都需要使线程间能够互相发送信号。
8、Java线程的通信方式(***)?
线程通信主要可以分为三种方式,分别为共享内存、消息传递和管道流。
1、共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。
volatile共享内存
2、消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。
等待/通知机制、join方式、Condition
3、管道流:管道输入/输出流的形式。
9、volatile关键字的作用(***)?
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。
所有volatile修饰的变量一旦被某个线程更改,必须立即刷新到主内存;
所有volatile修饰的变量在使用之前必须重新读取主内存的值;
volatile能保证所修饰的变量对于多个线程可见性,即只要被修改,其它线程读到的一定是最新的值。
10、sleep() 和 wait()的异同(***)?
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()。
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中。
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
11、线程中notify和notifyAll方法的作用(*)?
notify和notifyAll方法是一个native方法,并且都是final的,不允许子类重写。
notify是唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念),选择是任意性的;
notifyAll是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
notify和notifyAll只能在拥有对象监视器的所有者线程中调用,否则会抛出IllegalMonitorStateException异常
12、Condition机制和等待通知机制区别?
使用Condition往往比使用传统的通知等待机制(Object的wait()/notify())要更灵活、高效,我们可以使用多个Condition实现通知部分线程: