一. 线程安全
1.1 线程安全问题是什么,发生的原因
- 多个线程同时修改同一共享资源的时候,会出现线程安全问题。
- 读数据是绝对不会出现线程安全问题的,它一定是因为同时在修改。
- 一旦线程同步了,就是解决了安全问题了。
- CPU负责调度线程执行的,它是控制中心。
- 多线程访问临界资源时数据的安全问题,产生原因:有多个线程在同时访问同一共享资源,如果一个线程在取值的时候,时间片又被其他的线程抢走了,临界资源问题就产生了!
线程安全问题出现的原因?
- 存在多线程并发
- 同时访问并存在修改同一共享资源
1.2 线程安全问题案例模拟
package com.gch.d3_thread_safe;
/**
定义账户类
*/
public class Account {
private double money; // 账户的余额
public Account(double money) {
this.money = money;
}
public Account() {
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
/**
* 取钱功能
* @param money:取钱的金额
*/
public void drawMoney(double money){
// 1.先获取是谁来取钱,线程的名字设置的是人名
String name = Thread.currentThread().getName();
// 2.判单账户的余额 >= 取钱的金额
if(this.money >= money){
// 可以取钱了
System.out.println(name + "取钱成功,取出" + money + "元!");
// setMoney(getMoney() - money);
// 更新余额
this.money -= money;
System.out.println(name + "取钱后共享账户剩余:" + this.money);
}else{
System.out.println(name + "来取钱,账户余额不足");
}
}
}
package com.gch.d3_thread_safe;
/**
取钱的线程类
*/
public class DrawThread extends Thread {
// 接收处理的账户对象
private Account acc;
/**
* 有参构造器
* @param acc:接共享的账户对象
* @param name:线程名
*/
public DrawThread(Account acc,String name) {
super(name);
this.acc = acc;
}
public DrawThread() {
}
public Account getAcc() {
return acc;
}
public void setAcc(Account acc) {
this.acc = acc;
}
@Override
public void run() {
// 小明、小红:取钱
acc.drawMoney(100000);
}
}
package com.gch.d3_thread_safe;
/**
需求:模拟取钱案例
*/
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
// 1.定义账户类,创建一个账户对象代表2个人共享的账户对象
Account acc = new Account(100000);
// 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了
// 直接new对象这叫匿名对象
new DrawThread(acc,"小明").start();
// DrawThread.sleep(30);
new DrawThread(acc,"小红").start();
}
}
1. 线程安全问题发生的原因是什么?
- 多个线程同时访问同一共享资源且存在修改该资源。
线程安全问题模拟案例二:卖票 可能出现重复票和超卖的问题
-
案例需求
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
package com.gch.d3_thread_safe;
public class MyThread extends Thread {
/**
* 调用父类的有参构造器
* @param name:线程名
*/
public MyThread(String name){
super(name);
}
public static int ticket = 1; // 1 ~ 100
@Override
public void run() {
while(true){
if(ticket > 100){
// 卖完了
break;
}else{
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(getName() + "正在卖第" + ticket + "张票!");
ticket++;
}
}
}
}
package com.gch.d3_thread_safe;
public class ThreadDemo2 {
public static void main(String[] args) {
// 需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,
// 请设计一个程序模拟该电影院卖票
// 1.创建线程对象
Thread t1 = new MyThread("窗口1");
Thread t2 = new MyThread("窗口2");
Thread t3 = new MyThread("窗口3");
// 2.开启线程
t1.start();
t2.start();
t3.start();
}
}
- 出现重复票的根本原因是:线程在执行的时候,它是具有随机性的,CPU的执行权有可能随时会被其他的线程给抢走,还没来得及去打印,CPU的执行权就被其他的线程给抢走了。
- 线程在执行的时候,它是具有随机性的,CPU的执行权随时有可能会被其他的线程给抢走!
二. 线程同步机制
2.1 同步思想概述
线程同步
- 为了解决线程安全问题。
什么是线程同步?
- 就是一前一后的执行,不是竞争执行的。
1. 取钱案例出现问题的原因?
- 多个线程同时执行,发现账户都是够钱的。
2. 如何才能保证线程安全呢?
- 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
- 把操作共享数据的代码给锁起来!
线程同步的核心思想
- 加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。其他线程就算抢夺到了CPU的执行权,它也得在外面等着,它进不来!让所有的线程在核心代码当中能轮流执行!
- 注意:synchronized的锁对象,它一定要是唯一的!如果锁对象不唯一,导致一个线程进一个锁,那么这个锁就没有意义了!
锁:
- JVM,Java虚拟机级别的锁,同一个Java虚拟机里面同一个Java程序才能生效。
- 锁:对象锁、类锁(类锁也是对象锁,它是一种特殊的对象锁)
- 对象锁,就是任何一个实例对象,都可以看成是一把锁。
- 类锁:类名.class,任何一个类,只要类是相同的,那么它们的类名.class是唯一的实例对象!
- 如何保证线程的安全?
- 线程同步,本质就是让执行访问共享资源这段代码的时候,只有1个线程可以执行。
- 可以让线程之间通信,线程通信也能解决线程安全问题。
- 那么考虑一个问题:线程同步解决线程安全问题了有什么缺点吗?
- 运行速度变慢,因为同一时间只有一个线程在执行的,本来是3个5个窗口一起卖票的,而现在呢是一个窗口一个窗口卖,我卖的时候其他窗口给我暂停卖票,我卖完了你才能卖,这样的话性能会降低!
- 总结:多线程同步解决线程安全问题的缺点:性能会降低!
线程同步解决安全问题的思想是什么?
- 加锁:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
2.2 方式一:同步代码块:
利用synchronized关键字把操作共享数据的代码给锁起来,让同步代码块里面的代码是轮流去执行的!
注意:不同的线程必须加同一把锁!
锁对象的两个特点:
- 特点1:锁默认打开,有一个线程进去了,锁自动关闭
- 特点2:里面的代码全部执行完毕,线程出来,锁自动打开
锁对象用任意唯一的对象好不好呢?
- 不好,会影响其他无关线程的执行。
锁对象的规范要求
- 规范上:建议使用共享资源作为锁对象。
- 锁对象使用当前类的字节码文件!因为当前类的字节码文件对象一定是唯一的!
- 对于实例方法建议使用this作为锁对象。
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
package com.gch.d5_thread_synchronized_code;
/**
定义账户类
*/
public class Account {
private double money; // 账户的余额
public Account(double money) {
this.money = money;
}
public Account() {
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
/**
* 取钱功能
* @param money:取钱的金额
*/
public void drawMoney(double money){
// 1.先获取是谁来取钱,线程的名字设置的是人名
String name = Thread.currentThread().getName();
// 同步代码块
// 小明,小红 唯一的同步锁对象
// 规范上,建议使用共享资源作为锁对象 this = acc 共享账户
// 对于实例方法建议使用this作为锁对象
synchronized (this) { // acc.drawMoney(100000);
// 2.判单账户的余额 >= 取钱的金额
if(this.money >= money){
// 可以取钱了
System.out.println(name + "取钱成功,取出" + money + "元!");
// setMoney(getMoney() - money);
// 更新余额
this.money -= money;
System.out.println(name + "取钱后共享账户剩余:" + this.money);
}else{
System.out.println(name + "来取钱,账户余额不足");
}
}
}
}
package com.gch.d5_thread_synchronized_code;
/**
取钱的线程类
*/
public class DrawThread extends Thread {
// 接收处理的账户对象
private Account acc;
/**
* 有参构造器
* @param acc:接共享的账户对象
* @param name:线程名
*/
public DrawThread(Account acc, String name) {
super(name);
this.acc = acc;
}
public DrawThread() {
}
public Account getAcc() {
return acc;
}
public void setAcc(Account acc) {
this.acc = acc;
}
@Override
public void run() {
// 小明、小红:取钱
acc.drawMoney(100000);
}
}
package com.gch.d5_thread_synchronized_code;
/**
需求:模拟取钱案例
*/
public class ThreadSafeDemo {
public static void main(String[] args) throws InterruptedException {
// 测试线程安全问题
// 1.定义账户类,创建一个账户对象代表2个人共享的账户对象
Account acc = new Account(100000);
// 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了
// 直接new对象这叫匿名对象
new DrawThread(acc,"小明").start();
// DrawThread.sleep(30);
new DrawThread(acc,"小红").start();
}
}
- 同步代码块是如何实现线程安全的?
- 对出现问题的核心代码使用synchronized进行加锁
- 每次只能一个进程占锁进行访问
- 同步代码块的同步锁对象有什么要求?
- 对于实例方法建议使用this作为锁对象
- 对于静态方法建议使用(当前类的字节码文件对象)字节码(类名.class)对象作为锁对象,字节码文件对象一定是唯一的!
卖票案例加同步代码块!
注意:不要把synchronized放在死循环的外面,这样导致一个线程进来以后一直是当前线程在卖票,直到这个线程窗口卖完票才退出,导致其他窗口没有机会!
package com.gch.d3_thread_safe;
public class MyThread extends Thread {
/**
* 调用父类的有参构造器
* @param name:线程名
*/
public MyThread(String name){
super(name);
}
// 表示这个类所有的对象,都共享ticket数据
public static int ticket = 1; // 1 ~ 100
// 锁对象,一定要是唯一的
// public static Object obj = new Object();
@Override
public void run() {
while(true){
// 同步代码块 锁对象用当前类的字节码文件,当前类的字节码文件对象一定是唯一的!
synchronized (MyThread.class) {
if(ticket > 100){
// 卖完了
break;
}else{
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(getName() + "正在卖第" + ticket + "张票!");
ticket++;
}
}
}
}
}
package com.gch.d3_thread_safe;
public class ThreadDemo2 {
public static void main(String[] args) {
// 需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,
// 请设计一个程序模拟该电影院卖票
// 1.创建线程对象
Thread t1 = new MyThread("窗口1");
Thread t2 = new MyThread("窗口2");
Thread t3 = new MyThread("窗口3");
// 2.开启线程
t1.start();
t2.start();
t3.start();
}
}
2.3 方式二:同步方法
同步方法
- 作用:把出现线程安全问题的核心方法给上锁。
- 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
同步方法的两个特点:
- 特点1:同步方法是锁住方法里面所有的代码
- 特点2:同步方法的锁对象不能自己指定,是Java已经规定好的。如果当前方法是非静态的,那么锁对象就是this,也就是当前方法的调用者;如果当前方法是静态的,那么锁对象是当前类的字节码文件对象!
同步方法底层原理
- 同步方法其实底层也是有隐式锁对象的(只是我们看不到而已),只是锁的范围是整个方法代码块。
- 如果方法是实例方法:同步方法默认使用this作为锁对象。但是代码要高度面向对象!
- 如果方法是静态方法:同步方法默认使用类名.class作为锁对象。
同步代码块好还是同步方法好一点儿?
- 同步代码块锁的范围更小,锁的范围小,性能更好一点儿,而同步方法锁的范围更大。
- 比如上厕所,一个是在坑位门口锁,一个是在厕所门口锁
- 但是在实际开发中,同步方法比同步代码块用的更多一点,因为同步方法它的可读性好,写法方便。
- 官方(JDK)的源码也在大量使用同步方法,比如HashTable。
package com.gch.d6_thread_synchronized_method;
/**
定义账户类
*/
public class Account {
private double money; // 账户的余额
public Account(double money) {
this.money = money;
}
public Account() {
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
/**
* 取钱功能
* @param money:取钱的金额
*/
public synchronized void drawMoney(double money){
// 1.先获取是谁来取钱,线程的名字设置的是人名
String name = Thread.currentThread().getName();
// 2.判单账户的余额 >= 取钱的金额
if(this.money >= money){
// 可以取钱了
System.out.println(name + "取钱成功,取出" + money + "元!");
// setMoney(getMoney() - money);
// 更新余额
this.money -= money;
System.out.println(name + "取钱后共享账户剩余:" + this.money);
}else{
System.out.println(name + "来取钱,账户余额不足");
}
}
}
package com.gch.d6_thread_synchronized_method;
/**
取钱的线程类
*/
public class DrawThread extends Thread {
// 接收处理的账户对象
private Account acc;
/**
* 有参构造器
* @param acc:接共享的账户对象
* @param name:线程名
*/
public DrawThread(Account acc, String name) {
super(name);
this.acc = acc;
}
public DrawThread() {
}
public Account getAcc() {
return acc;
}
public void setAcc(Account acc) {
this.acc = acc;
}
@Override
public void run() {
// 小明、小红:取钱
acc.drawMoney(100000);
}
}
package com.gch.d6_thread_synchronized_method;
/**
需求:模拟取钱案例
*/
public class ThreadSafeDemo {
public static void main(String[] args) throws InterruptedException {
// 测试线程安全问题
// 1.定义账户类,创建一个账户对象代表2个人共享的账户对象
Account acc = new Account(100000);
// 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了
// 直接new对象这叫匿名对象
new DrawThread(acc,"小明").start();
// DrawThread.sleep(30);
new DrawThread(acc,"小红").start();
}
}
- 同步方法是如何保证线程安全的?
- 对出现问题的核心方法使用synchronized修饰
- 每次只能一个线程占锁进入访问
- 同步方法的同步锁对象的原理?
- 同步方法的底层是有隐式锁对象的,只是锁的范围是整个方法代码块!
- 对于实例方法默认使用this作为锁对象。
- 对于静态方法默认使用当前类的字节码文件,类名.class对象作为锁对象
同步方法案例二:卖票
- 不要去写同步方法,先写同步代码块,然后再把同步代码块里面的代码,去抽取成方法,这就OK了!
package com.gch.d3_thread_safe_2;
/**
线程任务类
*/
public class MyRunnable implements Runnable {
// MyRunnable对象只创建一次,因此变量票数面前无需加static
int ticket = 0; // 0 ~ 99
@Override
public void run() {
// 1.循环
while(true){
// 2.同步方法
if (method()) break;
}
}
// 锁对象:this
private synchronized boolean method() {
// 3.判断共享数据是否到了末尾,如果到了末尾
if(ticket == 100){
return true;
}else{
// 4.判断共享数据是否到了末尾,如果没有到末尾
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!" );
}
return false;
}
}
package com.gch.d3_thread_safe_2;
public class ThreadDemo {
public static void main(String[] args) {
/*
案例需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,
请设计一个程序模拟该电影院卖票
*/
// 1.创建线程任务对象
Runnable target = new MyRunnable();
// 2.创建线程对象
Thread t1 = new Thread(target,"窗口1");
Thread t2 = new Thread(target,"窗口2");
Thread t3 = new Thread(target,"窗口3");
// 3.启动线程
t1.start();
t2.start();
t3.start();
}
}
补充知识:StringBuilder和StringBuffer的区别
- StringBuilder和StringBuffer的API一模一样。
- StringBuilder是线程不安全的,StringBuffer是线程安全的。
- StringBuffer的源码里面方法都是同步方法,加了synchronized修饰。
- 使用场景区分:
- 如果你的代码是单线程的,不需要考虑多线程当中数据安全的抢矿,你就用StringBuilder就可以了。
- 如果说你是多线程环境下需要考虑数据安全,那么就可以选择StringBuffer。
2.4 方式三:Lock锁
- 有了Lock锁我们就可以手动的上锁,还有手动的释放锁了!
- ReentrantLock:重入锁 / 递归锁
- 重入锁是指的是如果一个同步方法或者一个同步代码块里面,还有相同对象的锁,那么第二次以后,就不必争抢锁,可以直接进入
package com.gch.d7_thread_synchronized_lock;
/**
重入锁是指的是如果一个同步方法或者一个同步代码块里面,还有相同对象的锁,那么第二次以后,就不必争抢锁,可以直接进入!
重入锁可以避免死锁! 还可以避免线程二次争抢锁!
*/
public class Demo {
public static void main(String[] args) {
// 1.定义锁对象
String lock = "ReentrantLock";
// 2.同步代码块
synchronized(lock){
System.out.println("这是第1把锁管辖范围!");
synchronized (lock){ // 可以直接进入,这个是因为锁都是同一把!
System.out.println("这是第2把锁管辖范围!");
}
}
}
}
- 重入锁可以避免死锁,还可以避免线程二次争抢锁!
package com.gch.d7_thread_synchronized_lock;
/**
重入锁还可以避免线程二次争抢锁!
*/
public class Demo2 {
public static void main(String[] args) {
// 1.定义锁对象
String lock = "ReentrantLock";
// 2.创建线程对象并开启线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized(lock){
System.out.println(Thread.currentThread().getName() + ":这是第一把锁管辖范围!");
synchronized(lock){ // 第二次直接无视这把锁,因为这把锁跟外面那把锁是一样的,是同一把锁
System.out.println(Thread.currentThread().getName() +":这是第二把锁管辖范围!");
}
}
}
},"A线程").start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized(lock){
System.out.println(Thread.currentThread().getName() +":这是第一把锁管辖范围!");
synchronized(lock){
System.out.println(Thread.currentThread().getName() +":这是第二把锁管辖范围!");
}
}
}
},"B线程").start();
}
}
- ReentrantLock实现了Lock接口,使用ReentrantLock可以显式的加锁和释放锁,而且还可以精准唤醒指定的线程。
- ReentrantLock最重要的两个方法:上锁和解锁。
- 使用ReentrantLock不可以贸贸然的上锁和解锁,必须要保证这个锁,无论如何都能解锁,避免造成死锁等待!
- 有lock()方法就必须要出现unlock()方法,它们是成对出现的!
- 注意:将unlock的操作放到finally块中!
package com.gch.d7_thread_synchronized_lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
定义账户类
*/
public class Account {
private double money; // 账户的余额
// 加了final的变量只能被初始化一次!
// final修饰后:锁对象是唯一和不可替换的,非常专业
private final Lock lock = new ReentrantLock(); // 实例成员变量,每创建一个账户对象就创建一个锁对象
public Account(double money) {
this.money = money;
}
public Account() {
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
/**
* 取钱功能
* @param money:取钱的金额
*/
public void drawMoney(double money){
// lock = null; 直接报错,因为锁对象被final修饰,是唯一的不可替换的
// 1.先获取是谁来取钱,线程的名字设置的是人名
String name = Thread.currentThread().getName();
// 2.判单账户的余额 >= 取钱的金额
lock.lock(); // 上锁
try {
if(this.money >= money){
// 可以取钱了
System.out.println(name + "取钱成功,取出" + money + "元!");
// setMoney(getMoney() - money);
// 更新余额
this.money -= money;
System.out.println(name + "取钱后共享账户剩余:" + this.money);
}else{
System.out.println(name + "来取钱,账户余额不足");
}
} finally { // 加了finally,解锁更加安全,即使出了bug也会解锁
lock.unlock(); // 解锁
}
}
}
package com.gch.d7_thread_synchronized_lock;
/**
取钱的线程类
*/
public class DrawThread extends Thread {
// 接收处理的账户对象
private Account acc;
/**
* 有参构造器
* @param acc:接共享的账户对象
* @param name:线程名
*/
public DrawThread(Account acc, String name) {
super(name);
this.acc = acc;
}
public DrawThread() {
}
public Account getAcc() {
return acc;
}
public void setAcc(Account acc) {
this.acc = acc;
}
@Override
public void run() {
// 小明、小红:取钱
acc.drawMoney(100000);
}
}
package com.gch.d7_thread_synchronized_lock;
/**
需求:模拟取钱案例
*/
public class ThreadSafeDemo {
public static void main(String[] args) throws InterruptedException {
// 测试线程安全问题
// 1.定义账户类,创建一个账户对象代表2个人共享的账户对象
Account acc = new Account(100000);
// 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了
// 直接new对象这叫匿名对象
new DrawThread(acc,"小明").start();
// DrawThread.sleep(30);
new DrawThread(acc,"小红").start();
}
}
Lock锁案例二:卖票
package com.gch.d3_thread_safe_3;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread extends Thread {
/**
* 调用父类Thread的有参构造器
* @param name:线程名
*/
public MyThread(String name){
super(name);
}
// 表示本类 / 当前类的所有对象,都共享ticket数据
public static int ticket = 0; // 0 ~ 99
// 定义Lock锁,锁对象必须是唯一和不可替换的
// 静态成员变量,本类 / 当前类的所有对象共享一把锁
// 如果定义成实例成员变量,那么每创建一个线程对象就创建一把锁,窗口1,窗口2,窗口3各自都有各自的锁,各自卖各自的
private static final Lock lock = new ReentrantLock();
@Override
public void run() {
// 1.循环
while(true){
// 2.上锁
lock.lock();
try {
// 3.判断
if(ticket == 100){
break; // 如果ticket == 100,将会直接跳出循环,这导致的结果就是没有释放锁!!!程序运行就会出现bug
}else{
// 4.判断
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!");
}
} finally {
// 4.解锁
lock.unlock();
}
}
}
}
package com.gch.d3_thread_safe_3;
public class ThreadDemo {
public static void main(String[] args) {
/*
案例需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,
请设计一个程序模拟该电影院卖票
用JDK5的Lock实现
*/
// 1.创建线程对象
Thread t1 = new MyThread("窗口1");
Thread t2 = new MyThread("窗口2");
Thread t3 = new MyThread("窗口3");
// 2.启动线程
t1.start();
t2.start();
t3.start();
}
}