线程安全
线程安全:当多个线程访问某一个类资源时,不论线程是何种的调度方式或者说线程是如何交替执行,并且在程序中没有额外的同步或协同。此时我们对类资源进行修改或读取,最终程序给出的结果就是我们预期的反馈,这个时候我们称这个类是线程安全的
线程安全主要体现在三个方面:1. 原子性 2. 可见性 3. 有序性
了解线程安全
经典案例
- 小明和小红是一家人,共用一个银行账户。小明和小红同时拿存折和银行卡去取钱。
账户类Account
public class Account {
private double leftMoney;
private String accountId;
public Account() {
}
public Account(double leftMoney, String accountId) {
this.leftMoney = leftMoney;
this.accountId = accountId;
}
public double getLeftMoney() {
return leftMoney;
}
public void setLeftMoney(double leftMoney) {
this.leftMoney = leftMoney;
}
public String getAccountId() {
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public void drawMoney( double money) {
//谁来取钱
String name = Thread.currentThread().getName();
System.out.println(name+"前来取钱!"+money);
if (money <= leftMoney){
//取钱成功
System.out.println(name+"取钱,余额足够,取钱成功,吐钱中~"+money);
//刷新余额
leftMoney -= money;
//打印取钱日志
System.out.println(name+"成功取出"+money+" 余额为:" + leftMoney);
}else{
System.out.println(name+"钱不够!!!");
}
}
}
取钱线程类Draw
//线程类
public class Draw implements Runnable{
public Account acc;
public Draw() {
}
//将账户对象传入,保证小明小红操作的是同一账户 即共享资源
public Draw(Account acc) {
this.acc = acc;
}
@Override
public void run() {
//默认全部取出
acc.drawMoney(100000);
}
}
测试类Test
public class Test {
public static void main(String[] args) {
//创建账户 余额十万
Account account = new Account(100000.0,"ICBC_SB1314");
//创建Runnable实现类对象
Draw all = new Draw(account);
//创建小明取钱线程
Thread Ming = new Thread(all);
Ming.setName("小明");
//创建小红取钱线程
Thread Red = new Thread(all);
Red.setName("小红");
//线程启动
Ming.start();
Red.start();
}
}
测试结果:
/*
第一次运行:
小明前来取钱!100000.0
小红前来取钱!100000.0
小明取钱,余额足够,取钱成功,吐钱中~100000.0
小红取钱,余额足够,取钱成功,吐钱中~100000.0
小明成功取出100000.0 余额为:0.0
小红成功取出100000.0 余额为:-100000.0
===============================================
第二次:
小红前来取钱!100000.0
小明前来取钱!100000.0
小明取钱,余额足够,取钱成功,吐钱中~100000.0
小明成功取出100000.0 余额为:0.0
小红取钱,余额足够,取钱成功,吐钱中~100000.0
小红成功取出100000.0 余额为:-100000.0
*/
- 此时就是我们所创建的账户由于被小明和小红共享而出现线程的安全问题
线程同步
为了解决线程安全问题,引入线程同步
核心思想:让多个线程实现先后依次访问共享资源
做法:将共享资源进行上锁,每次只能有一个线程计入访问,访问完毕以后其他线程才能进来
解決多线程同步问题:
- 同步代码块
- 同步方法
- lock显式加锁
同步代码块
格式:
synchronized(锁对象){
//访问共享资源的核心代码
}
/*
synchronized这个玩意儿也可以叫做同步监视器
锁对象:理论上可以是任意的唯一对象
建议锁对象为共享资源
在实例方法(非静态方法)中,建议使用this作为锁对象,此时锁对象就是我们的共享资源。但必须保证程序是高度面向对象
在静态方法中建议用类名.class字节码作为锁对象
*/
- 上述线程安全实例,利用synchronized同步代码块解决线程安全问题
- 取钱方法如下:
public void drawMoney( double money) {
//谁来取钱
String name = Thread.currentThread().getName();
System.out.println(name+"前来取钱!"+money);
synchronized(this) {//设置同步监视器 为共享资源上锁 每次只有一个线程可以进入
if (money <= leftMoney) {
//取钱成功
System.out.println(name + "取钱,余额足够,取钱成功,吐钱中~" + money);
//刷新余额
leftMoney -= money;
//打印取钱日志
System.out.println(name + "成功取出" + money + " 余额为:" + leftMoney);
} else {
System.out.println(name + "钱不够!!!");
}
}
//当前线程进入并执行完毕 则会释放同步锁,下一个线程可以进入
}
同步方法
与同步代码块类似
在有可能出现线程安全的核心方法中添加synchronized修饰符(将此方法整体上锁)
- 用法:
public synchronized void drawMoney( double money) {
//谁来取钱
String name = Thread.currentThread().getName();
System.out.println(name+"前来取钱!"+money);
if (money <= leftMoney) {
//取钱成功
System.out.println(name + "取钱,余额足够,取钱成功,吐钱中~" + money);
//刷新余额
leftMoney -= money;
//打印取钱日志
System.out.println(name + "成功取出" + money + " 余额为:" + leftMoney);
} else {
System.out.println(name + "钱不够!!!");
}
}
显示加锁lock
Lock被称为同步锁,加锁与解锁都被方法化
加锁:- - public void lock()
解锁:- - public void unlock()
在加锁与解锁过程中注意处理异常
- 重入锁ReentrantLock
- 可重入锁ReentrantLock 是一个互斥锁,即同一时间只有一个线程能够获取锁定资源,执行锁定范围内的代码。这一点与synchronized 关键字十分相似。
- ReentrantLock可重入锁,即:(lock/unlok)动作里面可以嵌套(lock/unlock),针对同一个锁可以多次嵌套使用,不会产生死锁。但是lock函数与unlock函数在代码中必须成对出现,否则会出现死锁。
用法:
private Lock lock = new ReentrantLock();
public void drawMoney( double money) {
//谁来取钱
String name = Thread.currentThread().getName();
lock.lock;//上锁
try{
System.out.println(name+"前来取钱!"+money);
if (money <= leftMoney) {
//取钱成功
System.out.println(name + "取钱,余额足够,取钱成功,吐钱中~" + money);
//刷新余额
leftMoney -= money;
//打印取钱日志
System.out.println(name + "成功取出" + money + " 余额为:" + leftMoney);
} else {
System.out.println(name + "钱不够!!!");
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();//解锁
}
}
线程控制
join方法
- Thread 提供了让 一个线程等待另 一个线程完成的方法-----join()方法 。 当在某个程序执行流中调用其他线程的 join()方法时,调用线程将被阻塞 , 直到被 join()方法加入的线程执行完为止
join方法重載:
--- join() 等待调用该方法的线程死亡
--- join(long millis) 等待调用该方法的线程死亡最多millis毫秒时间
--- join(long millis,int nanos) 与2相同,nanos为毫秒
/**
* join方法使用
*/
class MyThread extends Thread{
public MyThread(String name){
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(getName()+i);
}
}
}
public class Demo01 {
public static void main(String[] args) {
Thread.currentThread().setName("main主线程");
MyThread myThread = new MyThread("被join的子线程");
Thread thread = new Thread(myThread);
for (int i = 1; i <= 10; i++) {
if(i ==5){
thread.start();
try {
thread.join();//被join的线程执行完放CPU给main线程继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + i);
}
}
}
后台线程
- 该线程在后台运行,任务是为其他线程提供服务,该线程被称为后台线程
- 如果前台线程全部死亡,后台线程会自动死亡
public class DaemonThread extends Thread
{
// 定义后台线程的线程执行体与普通线程没有任何区别
public void run()
{
for (int i = 0; i < 1000 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
DaemonThread t = new DaemonThread();
// 将此线程设置成后台线程
t.setDaemon(true);
// 启动后台线程
t.start();
for (int i = 0 ; i < 10 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
// -----程序执行到此处,前台线程(main线程)结束------
// 后台线程也应该随之结束
}
}
线程睡眠
sleep()静态方法是让当前线程进入休眠状态,让出CPU资源,处于阻塞状态,但是不会释放锁。则可以通过调用 Thread 类的静态 sleep()方法来实现。
sleep()方法具有两种重载形式:
- sleep(long millis) 指定睡眠的毫秒数
- sleep(long millis , int nanos)指定睡眠毫秒和纳秒数
线程让步
Thread还提供了一个yield()静态方法,yield()方法可以让当前线程暂停,但不会使其进入阻塞状态而是进入可运行即就绪状态,暂停结束后再次与其他线程抢占CPU资源
yield()方法与sleep()方法区别;
yield()方法调用后,线程进入就绪状态。sleep()方法调用后,线程进入阻塞状态
Volatile关键字
Volatile保证了线程间的可见性
解释:可见性又叫读写可见。即一个共享变量N,当有两个线程T1、T2同时获取了N的值,T1修改N的值,而T2读取N的值,可见性规范要求T2读取到的值必须是T1修改后的值。
- 进入死循环
- 利用volatile跳出循环
class Tunnel extends Thread{
public static volatile boolean judge = true;
@Override
public void run() {
while(judge){
}
}
}
public class Demo01 {
public static void main(String[] args) {
Tunnel tunnel = new Tunnel();
new Thread(tunnel).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Tunnel.judge = false;
}
}
Java内存模型(JMM)
- 每个线程创建时,JVM会为其创建一份私有的工作内存(栈空间),不同线程的工作内存之间不能直接互相访问。
- JMM规定所有的变量都存在主内存,主内存是共享内存区域,所有线程都可以访问。
- 线程对变量进行读写,会从主内存拷贝一份副本到自己的工作内存,操作完毕后刷新到主内存。所以,线程间的通信要通过主内存来实现。
- JMM内存模型的可见性是指,多线程访问主内存的某一个资源时,如果某一个线程在自己的工作内存中修改了该资源,并写回主内存,那么JMM内存模型应该要通知其他线程来从新获取最新的资源,来保证最新资源的可见性。
线程通信
线程通信一定是在多个线程操作同一资源时才会需要进行通信,此时则必须要保证线程安全
生产者与消费者模型
三种核心方法
- wait()
导致当前线程等待,直到其他线程调用该同步监视器的notify()或notifyAll()方法来唤醒该线程,调用wait()方法会释放当前线程的锁。
-
notify()
唤醒在此同步监视器上等待的单个线程,如果有多个线程在等待,则随机选择其中一个唤醒。只有当前线程使用了wait方法,放弃了该锁,此时才会有其他线程被唤醒 -
notifyAll()
唤醒在这个同步监视器上等待的所有线程
案例:存款取款、小明和小红取款、三个爸爸存款
案例实现:设置存钱线程和取钱线程,并在账户类设置取钱和存钱方法(需要上锁,设置同步监视器),在存取钱的方法内要设置无论存取是否成功要设置当前线程 wait睡眠,在睡眠之前调用natify或者notifyAll方法唤醒其他线程。
账户类:
/**
* 账户类
*/
public class Account {
private String IDCard;
private double LeftMoney;
//存钱
public synchronized void saveMoney(double money) {
//来存钱得人
String name = Thread.currentThread().getName();
try {
if(LeftMoney <= 0){
LeftMoney += money;
//打印日志
System.out.println(name + "来存钱,存了" + money + ",余额:" + LeftMoney);
this.notifyAll();
this.wait();
}else{
System.out.println("有钱不用存");
this.notifyAll();
this.wait();
}
}catch(Exception e){
e.printStackTrace();
}
}
//取钱
public synchronized void drawMoney(double money){
//来取钱的人
String name = Thread.currentThread().getName();
try {
//判断钱够不够
if(LeftMoney >= money){
//更新余额
LeftMoney -= money;
//打印日志
System.out.println(name+"来取钱,取了"+money+",余额:"+LeftMoney);
this.notifyAll();
this.wait();
}else{
System.out.println("没钱");
this.notifyAll();
this.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public Account() {
}
public Account(String IDCard, double leftMoney) {
this.IDCard = IDCard;
this.LeftMoney = leftMoney;
}
}
存钱线程:
/**
* 存钱线程
*/
public class SaveMoneyThread extends Thread{
private Account account;
public SaveMoneyThread() {
}
public SaveMoneyThread(Account account , String name){
super(name);
this.account = account;
}
@Override
public void run() {
account.saveMoney(1000);
}
}
取钱线程
/**
* 取钱线程
*/
public class DrawMoneyThread extends Thread{
private Account account;
public DrawMoneyThread(Account account,String name){
super(name);
this.account = account;
}
@Override
public void run() {
account.drawMoney(1000);
}
}
测试类
/**
* 测试类
*/
public class TestMain {
public static void main(String[] args) {
Account acc = new Account("ICBC_1314",1000);
//俩取钱的
new DrawMoneyThread(acc,"明哥").start();
new DrawMoneyThread(acc,"红姐").start();
//仨存钱的
new SaveMoneyThread(acc,"父一").start();
new SaveMoneyThread(acc,"父二").start();
new SaveMoneyThread(acc,"父三").start();
}
}