一. 线程安全
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;}@Overridepublic 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@Overridepublic 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的锁对象,它一定要是唯一的!如果锁对象不唯一,导致一个线程进一个锁,那么这个锁就没有意义了!
线程同步解决安全问题的思想是什么?
- 加锁:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
2.2 方式一:同步代码块:
利用同步代码块把操作共享数据的代码给锁起来,让同步代码块里面的代码是轮流去执行的!
锁对象的两个特点:
- 特点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;}@Overridepublic 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();@Overridepublic 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;}@Overridepublic 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对象只创建一次,因此变量票数面前无需加staticint ticket = 0; // 0 ~ 99@Overridepublic void run() {// 1.循环while(true){// 2.同步方法if (method()) break;}}// 锁对象:thisprivate 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锁我们就可以手动的上锁,还有手动的释放锁了!
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;}@Overridepublic 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();@Overridepublic 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();}
}