Java多线程02(线程安全、线程同步、等待唤醒机制)
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类中
- 锁对象可以是任意类型的对象
Java多线程02(线程安全、线程同步、等待唤醒机制)的更多相关文章
- “全栈2019”Java多线程第二十四章:等待唤醒机制详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- Java多线程间通信-解决安全问题、等待唤醒机制
/*1.增加一个知识点一个类怎么在所有的类中,让其它类来共同修改它的数据呢?可以用单例设计模式可以用静态可以在其它类中做一个构造函数,接受同一个对象,这样就可以实现对象 2.状态选择可以用数字0 1 ...
- 《java多线程编程核心技术》不使用等待通知机制 实现线程间通信的 疑问分析
不使用等待通知机制 实现线程间通信的 疑问分析 2018年04月03日 17:15:08 ayf 阅读数:33 编辑 <java多线程编程核心技术>一书第三章开头,有如下案例: ...
- Android-Lock-多线程通讯(生产者 消费者)&等待唤醒机制
此篇博客以 生产面包
- JAVA之旅(十四)——静态同步函数的锁是class对象,多线程的单例设计模式,死锁,线程中的通讯以及通讯所带来的安全隐患,等待唤醒机制
JAVA之旅(十四)--静态同步函数的锁是class对象,多线程的单例设计模式,死锁,线程中的通讯以及通讯所带来的安全隐患,等待唤醒机制 JAVA之旅,一路有你,加油! 一.静态同步函数的锁是clas ...
- java ->多线程_线程同步、死锁、等待唤醒机制
线程安全 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的. l 我们通过一个案例,演示线 ...
- Java多线程(二) —— 线程安全、线程同步、线程间通信(含面试题集)
一.线程安全 多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的. 讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会 ...
- java基础(27):线程安全、线程同步、等待唤醒机制
1. 多线程 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的. 我们通过一个案例,演示线程 ...
- Java多线程(一) —— 线程的状态详解
一.多线程概述 1. 进程 是一个正在执行的程序.是程序在计算机上的一次运行活动. 每一个进程执行都有一个执行顺序.该顺序是一个执行路径,或者叫一个控制单元. 系统以进程为基本单位进行系统资源的调度 ...
随机推荐
- linux最小化安装后的初始化
Linux 最小化安装以后 linux会缺失很多功能,需要我们预先安装一些软件服务,例如mysql(mariadb),gcc等等. 但是最小化的mysql甚至不提供ifconfig,也没有wget命令 ...
- logging模块全总结
Python之日志处理(logging模块) 本节内容 日志相关概念 logging模块简介 使用logging提供的模块级别的函数记录日志 logging模块日志流处理流程 使用logging四 ...
- SurfaceView绘图时刷新问题,尝试各种办法无法解决,请教高手
/** * */ 源码:http://pan.baidu.com/s/1i3FtdZZ 画图时最左面,第一帧总是出现一个黑条,其它的帧没有问题package com.macrosoft.testewa ...
- jmeter向ActiveMQ发送消息_广播/订阅(Topics 队列)
问题描述:测试中需要模拟大量设备的消息上报到平台,但是实际测试中没有那么多设备,所以采取用jmeter直接往ActiveMQ模拟发送设备消息 解决思路:获取平台采取的是Queues还是Topics : ...
- 在c#中利用keep-alive处理socket网络异常断开的方法
本文摘自 http://www.z6688.com/info/57987-1.htm 最近我负责一个IM项目的开发,服务端和客户端采用TCP协议连接.服务端采用C#开发,客户端采用Delphi开发.在 ...
- IntelliJ IDEA 破解Jrebel6.3.0安装
首先下载所必要的两个文件(jrebel3.6.0和cracked文件) 密码:pvsd 注意:如果不是该版本的Jrebel该破解文件可能无效. 步骤1:安装 解压文件得出两个压缩包 在idea中选择 ...
- PG数据库——视图
视图(View)是从一个或多个表(或视图)导出的表.视图与表(有时为与视图区别,也称表为基本表——Base Table)不同,视图是一个虚表,即视图所对应的数据不进行实际存储,数据库中只存储视图的定义 ...
- Log4j日志框架学习零到壹(一)
日志是系统开发过程中用于排查问题重要的记录.通常使用日志来记录系统运行的行为,什么时间点发生了什么 事情.Java中常用的莫过于Log4j框架了.下面主要围绕Log4j的基础知识.Log4j的使用方式 ...
- 996 icu我能为你做什么?
今天996,未来icu 996icu地址:https://github.com/996icu/996.ICU 前段时间github上出现了,一个讨论996的项目,这个项目使中国的软件工程师达到了空前的 ...
- 1_Python历史及入门
前提:简述CPU 内存 硬盘 操作系统 应用程序CPU:计算机的运算核心和控制核心,好像人类的”大脑“内存:负责数据与CPU直接数据交流处理,将临时数据和应用程序加载到内存,然后在交由CPU处理. 造 ...