Java多线程2(线程安全、线程同步、等待唤醒机制、单例设计模式)
1、线程安全
-
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
- 通过案例演示线程的安全问题:电影院要卖票。
- 我们模拟电影院的卖票过程。假设本场电影的座位共100个(本场电影只能卖100张票)。
- 我们来模拟电影院的售票窗口,实现多个窗口同时卖这场电影的票(多个窗口一起卖这100张票)
- 需要窗口,采用线程对象来模拟;
- 需要票,Runnable接口子类来模拟;
代码:
public class Tickets implements Runnable {
private int num = 100; //(1)
@Override
public void run() { // (2)
// 死循环,一直处于可以售票状态
while(true) { // (3)
if(num>0) { // (4)
System.out.println(Thread.currentThread().getName()+" 第 "+ num-- + " 张票售出"); //(5)
}
}
}
}
public class TicketsDemo {
public static void main(String[] args) { //(6)
Tickets t = new Tickets(); // (7)
new Thread(t).start(); // (8)
new Thread(t).start(); // (9)
new Thread(t).start(); // (10)
}
}
-
分析:
- 三个窗口每个窗口都在买票,假设此时只剩一张票,可能会发生以下情况:
- 线程t1执行run方法到(4)时,产生阻塞,线程t2执行run方法到(4)时叶阻塞,线程t3执行完了run方法,释放CPU,此时num=0;t1再次得到CPU时,不会再次判断,而是直接执行下一步(5),这时就会发生0--,出现出售第0张票,并且票数变成负数,这样就出现了安全隐患。
-
运行结果发现:上面程序出现了问题
- 票出现了重复的票
- 错误的票 0、-1
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
-
解决办法:
- 当一个线程进入数据操作的时候,无论是否休眠,其他线程智能等待。
2、线程同步(线程安全处理Synchronized)
- java中提供了线程同步机制,它能够解决上述的线程安全问题。
- 线程同步的方式有两种:
- 方式1:同步代码块
- 方式2:同步方法
2.1 同步代码块
- 同步代码块: 在代码块声明上 加上synchronized
synchronized (锁对象) {
可能会产生线程安全问题的代码
}
同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:
/*
通过线程休眠,出现安全问题
解决安全问题,Java程序,提供同步技术
公式:
syncronized (任意对象){
线程要操作的共享数据
}
*/
public class Tickets implements Runnable {
// 定义出售的票数
private int num = 100;
Object obj = new Object(); // 创建对象,用于同步
@Override
public void run() {
// 死循环,一直处于可以售票状态
while(true) {
// 线程共享数据,保证安全,加入同步代码块
synchronized (obj) {
if(num>0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 第 "+ num-- + " 张票售出");
}
}
}
}
}
- 当使用了同步代码块后,上述的线程的安全问题,解决了。
- 分析:
- 同步对象:可以是任意对象,可以称之为同步锁,对象监视器,注意不能用匿名内部类,因为这样在会导致每次获得锁对象都是新的对象,无法实现加锁的效果。
- 同步是如何保证安全性的:没有锁的线程不能执行,只能等待。
- 具体执行过程:
- 线程遇到同步代码块后,线程判断同步锁还有没有
- 如果同步锁有:获取锁,进入同步中,去执行,执行完毕后,离开同步代码块,线程将锁对象还回去。
- 在同步中的线程休眠,此时另一个线程会执行;
- 遇到同步代码块,判断对象锁是否还有,如果没有锁,该线程不能进入同步代码块中执行,被阻挡在同步代码块的外面,处于阻塞状态。
- 加了同步之后,执行步骤增加:线程首先进同步判断锁,获取锁,出同步释放锁,导致程序运行速度的下降。
- 没有锁的线程,不能进入同步,在同步中的线程,不出同步,不会释放锁。
2.2 同步方法(推荐使用)
- 同步方法:在方法声明上加上synchronized
public synchronized void method(){
可能会产生线程安全问题的代码
}
- 同步方法中的锁对象是 this
- 使用同步方法,对电影院卖票案例中Ticket类进行如下代码修改:
/*
采用同步方法的形式解决线程安全问题
好处:代码量少,简洁
做法:将线程共享数据和同步抽取到方法中
*/
public class Tickets implements Runnable {
private int num = 100;
@Override
public void run() {
// 死循环,一直处于可以售票状态
while(true) {
payTicket();
}
}
public synchronized void payTicket() {
if(num>0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 第 "+ num-- + " 张票售出");
}
}
}
-
问题:同步方法中有锁吗?
- 有,同步方法中的对象锁是本类方法的引用
静态同步方法: 在方法声明上加上static synchronized
public static synchronized void method(){
// 可能会产生线程安全问题的代码
}
- 静态同步方法中的锁对象是本类自己:类名.class
1.4 Lock接口
- 查阅API,查阅Lock接口描述,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
- 实现类:
ReentrantLock
- Lock接口中的常用方法
-
void lock()
:获得锁。 -
void unlock()
:释放锁。
-
- Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。
- 实现类:
- 我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
使用JDK1.5+的接口Locl,替换同步代码块,实现线程安全
具体使用:
Lock接口中的方法:
lock(); // 获取锁
unlock(); // 释放锁
实现类:ReentrantLock
*/
public class Tickets implements Runnable {
// 存储票数
private static int num = 100;
//在类的成员位置,创建Lock接口的实现类对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
// 死循环,一直处于可以售票状态
while(true) {
// 调用Lock接口中的方法,获取锁
lock.lock();
try {
if(num>0) {
Thread.sleep(200);
System.out.println(Thread.currentThread().getName()+" 第 "+ num-- + " 张票售出");
}
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放锁,调用unlock方法
lock.unlock();
}
}
}
}
1.4 死锁
-
同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:
- 程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。
-
死锁程序:
- 前提:必须是多线程
- 出现同步嵌套
- 线程进入同步,获取锁,不出去同步,不会释放锁
-
锁的嵌套情况如下:
synchronzied(A锁){
synchronized(B锁){ }
}
synchronzied(B锁){
synchronized(A锁){ }
} 注意A锁和B锁都是唯一的
两个线程每个获得一个锁,且都需要对方的锁才能继续执行,因此都会一直除以阻塞状态,无法恢复,出现死锁。
-
我们进行下死锁情况的代码演示:
// 定义锁对象类
/*
不允许任何类创建该对象
只能通过类名调用静态成员调用,不允许new
保证了锁的唯一性
*/
public class LockA {
private LockA() {}
public final static LockA locka = new LockA();
} public class LockB {
private LockB() {}
public final static LockB lockb = new LockB();
} // 线程任务类
public class DeadLock implements Runnable{
private int i = 0;
@Override
public void run() {
while(true) {
if(i%2==0) {
// 先进入A同步,再进入B同步
synchronized (LockA.locka) {
System.out.println(i+" --> if...locka");
synchronized (LockB.lockb) {
System.out.println(i+" --> if...lockb");
}
}
}else {
// 先进入B同步,再进入B同步
synchronized (LockB.lockb) {
System.out.println(i+" --> else...lockb");
synchronized (LockA.locka) {
System.out.println(i+" --> else...locka");
}
}
}
i++;
}
}
} // 测试类
public class DeadLockDemo {
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
new Thread(deadLock).start();
new Thread(deadLock).start();
}
} // 运行结果:
0 --> if...locka
0 --> if...lockb
1 --> else...lockb
1 --> if...locka
1.5 等待唤醒机制
在开始讲解等待唤醒机制之前,有必要搞清一个概念—— 线程之间的通信。
线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
-
等待唤醒机制所涉及到的方法:
- wait() :等待,无限等待。将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
- notify() :唤醒。唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
- notifyAll() :唤醒全部:可以将线程池中的所有wait()线程都唤醒。
-
所谓唤醒:就是让线程池中的线程具备执行资格。
- 必须注意的是:这些方法都是在同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。
-
仔细查看Java API之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?
- 因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。
-
线程通讯案例:输入线程向Resource中输入name ,sex , 输出线程从资源中输出,先要完成的任务是:
- 当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();
- 当output发现Resource中没有数据时,就wait() ;当发现有数据时,就输出,然后,叫醒input来输入数据。
-
下面代码,模拟等待唤醒机制的实现:
-
Resource.java
/*
定义资源类,有2个成员变量:
name,sex
同时有两个线程,对资源中的变量操作
1个对name,sex赋值
1个对name,sex做变量的输出打印
*/
public class Resource {
public String name;
public String sex;
} -
Input.java
/*
输入线程:
对资源对象Resource中的成员变量赋值
要求:
一次赋值:张三,男
另一次:李四,女 */
public class Input implements Runnable {
private Resource r;
public Input(Resource r) {
this.r = r;
}
@Override
public void run() {
int i = 0;
while(true) {
if(i%2==0) {
r.name = "张三";
r.sex = "男";
}else {
r.name = "lisi";
r.sex = "nv";
}
i++;
}
}
} -
Output.java
/*
输出线程:对资源对象Resource中的成员变量输出值
*/
public class Output implements Runnable {
private Resource r;
public Output(Resource r) {
this.r = r;
}
@Override
public void run() {
while(true) {
System.out.println("姓名:"+r.name + ", 性别:"+r.sex);
}
} } -
ThreadDemo.java
/*
开启输入线程和输出线程,实现赋值和打印
*/
public class ThreadDemo {
public static void main(String[] args) {
Resource r = new Resource(); //共享数据
Input in = new Input(r);
Output out = new Output(r);
new Thread(in).start();
new Thread(out).start();
}
}
-
-
此时会出现问题:打印出的结果并不是想要的结果
姓名:lisi, 性别:nv
姓名:张三, 性别:nv
姓名:lisi, 性别:男
姓名:lisi, 性别:nv
姓名:lisi, 性别:nv
姓名:张三, 性别:男 分析原因,两个线程没有实现同步。
-
实现同步的方法:给线程加同步锁。
- 注意:给输入和输出加的同步锁应为同一个对象锁,而输入和输出线程是两个不同的线程,因此不能使用this作为对象锁,这里使用他们公用的资源类Resource对象。
-
代码修改如下:
-
Input.java修改
/*
输入线程:
对资源对象Resource中的成员变量赋值
要求:
一次赋值:张三,男
另一次:李四,女 */
public class Input implements Runnable {
private Resource r;
public Input(Resource r) {
this.r = r;
}
@Override
public void run() {
int i = 0;
while(true) {
synchronized (r) {
if(i%2==0) {
r.name = "张三";
r.sex = "男";
}else {
r.name = "lisi";
r.sex = "nv";
}
}
i++;
}
}
} -
Input.java修改
/*
输入线程:
对资源对象Resource中的成员变量赋值
要求:
一次赋值:张三,男
另一次:李四,女 */
public class Input implements Runnable {
private Resource r;
public Input(Resource r) {
this.r = r;
}
@Override
public void run() {
int i = 0;
while(true) {
synchronized (r) {
if(i%2==0) {
r.name = "张三";
r.sex = "男";
}else {
r.name = "lisi";
r.sex = "nv";
}
}
i++;
}
}
} -
Output.java修改:
/*
输出线程:对资源对象Resource中的成员变量输出值
*/
public class Output implements Runnable {
private Resource r;
public Output(Resource r) {
this.r = r;
}
@Override
public void run() {
while(true) {
synchronized (r) {
System.out.println("姓名:"+r.name + ", 性别:"+r.sex);
}
}
}
}
-
-
此时还有问题:输出没有交替进行
姓名:张三, 性别:男
姓名:张三, 性别:男
姓名:张三, 性别:男
姓名:lisi, 性别:nv
姓名:lisi, 性别:nv
姓名:lisi, 性别:nv
姓名:lisi, 性别:nv -
分析原因:
- 输入:输入完成以后,必须等待,等待输出打印结束后,才能进行下一次赋值。
- 输出:输出完变量值后,必须等待,等待输入的重新赋值后,才能进行下一次打印。
-
解决方法:
- 输入:赋值后,执行方法wait(),永远等待,
- 输出:变量打印输出,在输出等待之前,唤醒输入的nitify(),自己再wait等待。
- 输入:被唤醒后,重新对变量赋值,然后唤醒输出的线程notify,自己再wait()等待。
- 如何判断输入输出结束:设置一个标记flag,以标记为准;
- flag = false; 说明赋值完成
- flag = true; 获取值完成
- 输入操作:
- 需要不需要赋值,看标记
- 如果标记为true,等待
- 如果标记为false,不需要等待,赋值
- 赋值后,将标记改为true
- 输出操作:
- 需要不需要获取,看标记
- 如过标记为false,等待
- 如果标记为true,打印
- 打印后,将标记改为false
-
代码修改如下:
-
Resource.java修改
/*
定义资源类,有2个成员变量:
name,sex
同时有两个线程,对资源中的变量操作
1个对name,sex赋值
1个对name,sex做变量的输出打印
*/
public class Resource {
public String name;
public String sex;
public boolean flag = false;
} -
Input.java修改
/*
输入线程:
对资源对象Resource中的成员变量赋值
要求:
一次赋值:张三,男
另一次:李四,女 */
public class Input implements Runnable {
private Resource r;
public Input(Resource r) {
this.r = r;
}
@Override
public void run() {
int i = 0;
while(true) {
synchronized (r) {
if(r.flag) { // 标记是true,等待
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(i%2==0) {
r.name = "张三";
r.sex = "男";
}else {
r.name = "lisi";
r.sex = "nv";
}
// 标记改为true,将对方线程唤醒
r.flag = true;
r.notify();
}
i++;
}
}
} -
Output.java修改
/*
输出线程:对资源对象Resource中的成员变量输出值
*/
public class Output implements Runnable {
private Resource r;
public Output(Resource r) {
this.r = r;
}
@Override
public void run() {
while(true) {
synchronized (r) {
if(!r.flag) { // 判断标记,false,等待
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("姓名:"+r.name + ", 性别:"+r.sex);
r.flag = false;
r.notify();
}
}
}
}
-
-
注意:
- 等待和唤醒必须是由同一个对象调用,这里用Resource的对象
2 总结
同步锁
-
多个线程想保证线程安全,必须要使用同一个锁对象
-
同步代码块
synchronized (锁对象){
可能产生线程安全问题的代码
}
-
-
同步代码块的锁对象可以是任意的对象
-
同步方法
public synchronized void method()
可能产生线程安全问题的代码
}
// 同步方法中的锁对象是 this -
静态同步方法
public synchronized void method()
可能产生线程安全问题的代码
}
// 静态同步方法中的锁对象是 类名.class
-
多线程有几种实现方案,分别是哪几种?
- 继承Thread类
- 实现Runnable接口
- 通过线程池,实现Callable接口
同步有几种方式,分别是什么?
- 同步代码块
- 同步方法
- 静态同步方法
启动一个线程是run()还是start()?它们的区别?
- 启动一个线程是start()
- 区别:
- start: 启动线程,并调用线程中的run()方法
- run : 执行该线程对象要执行的任务
sleep()和wait()方法的区别
- sleep: 不释放锁对象, 释放CPU使用权;在休眠的时间内,不能唤醒
- wait(): 释放锁对象, 释放CPU使用权;在等待的时间内,能唤醒
为什么wait(),notify(),notifyAll()等方法都定义在Object类中
- 锁对象可以是任意类型的对象