- 多个线程同时修改同一共享资源的时候,会出现线程安全问题。
- 读数据是绝对不会出现线程安全问题的,它一定是因为同时在修改。
- 一旦线程同步了,就是解决了安全问题了。
- CPU负责调度线程执行的,它是控制中心。
线程安全问题出现的原因?
- 存在多线程并发
- 同时访问并存在修改同一共享资源
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的执行权随时有可能会被其他的线程给抢走!
线程同步
- 为了解决线程安全问题。
1. 取钱案例出现问题的原因?
- 多个线程同时执行,发现账户都是够钱的。
2. 如何才能保证线程安全呢?
- 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
- 把操作共享数据的代码给锁起来!
线程同步的核心思想
- 加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。其他线程就算抢夺到了CPU的执行权,它也得在外面等着,它进不来!让所有的线程在核心代码当中能轮流执行!
- 注意: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();
}
}
同步方法
同步方法的两个特点:
- 特点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的API一模一样。
- StringBuilder是线程不安全的,StringBuffer是线程安全的。
- StringBuffer的源码里面方法都是同步方法,加了synchronized修饰。
- 使用场景区分:
- 如果你的代码是单线程的,不需要考虑多线程当中数据安全的抢矿,你就用StringBuilder就可以了。
- 如果说你是多线程环境下需要考虑数据安全,那么就可以选择StringBuffer。
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();
}
}
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();
}
}