java中线程的一些相关概念,第4篇(线程同步、死锁、线程间的通信)
直接po代码和截图
源码点这里
我们根据一个案例来讲解线程同步的知识点!
案例如下:
张无忌和赵敏有一个共同的存钱罐,存钱罐中初始有1000元,然后张无忌存了10次钱,每次存100元,而赵敏取了10次钱,每次取50元,最终存钱罐还剩1500元
Zhangwuji类
package com.demo.thread2;
public class Zhangwuji implements Runnable{
private String name;
private MoneyBox moneyBox;
public Zhangwuji(String name, MoneyBox moneyBox){
this.name = name;
this.moneyBox = moneyBox;
}
@Override
public void run() {
add();
}
public void add() {
for (int frequency = 1; frequency <= 10; frequency++) {
System.out.println("#######我是" + name + ",这是我第" + frequency + "次存钱#######");
moneyBox.add(100, name, frequency);
}
}
}
Zhaomin类
package com.demo.thread2;
public class Zhaomin implements Runnable {
private String name;
private MoneyBox moneyBox;
public Zhaomin(String name, MoneyBox moneyBox) {
this.name = name;
this.moneyBox = moneyBox;
}
@Override
public void run() {
get();
}
public void get() {
for (int frequency = 1; frequency <= 10; frequency++) {
System.out.println("*******我是" + name + ",这是我第" + frequency + "次取钱*******");
moneyBox.get(50, name, frequency);
}
}
}
MoneyBox类
package com.demo.thread2;
//存钱罐类
public class MoneyBox {
//存钱罐中的余额
private double money;
public double getMoney() {
return money;
}
// 构造函数 /构造方法/构造器
public MoneyBox(double money) {
this.money = money;
}
//这是存钱的方法,暂时先不加synchronized(同步),先不写成同步方法,看下什么效果
public void add(double inMoney, String threadName, int number) {
double oldMoney = money;
try {
//拿到旧的金额后,让当前线程睡眠会以使别的线程操作它
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
money = oldMoney + inMoney;
System.out.println("-------" + threadName + "第" + number + "次存了" + inMoney + "元,现在存钱罐中的余额=" + money);
}
//这是取钱的方法,暂时先不加synchronized(同步),先不写成同步方法,看下什么效果
public void get(double outMoney, String threadName, int number) {
double oldMoney = money;
try {
//拿到旧的金额后,让当前线程睡眠会以使别的线程操作它
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
money = oldMoney - outMoney;
System.out.println("$$$$$$$" + threadName + "第" + number + "次取了" + outMoney + "元,现在存钱罐中的余额=" + money);
}
@Override
public String toString() {
return "---------------我是存钱罐,我现在的余额=" + money + "---------------";
}
}
Test类
package com.demo.thread2;
public class Test {
//测试线程同步
public static void main(String[] args) {
//存钱罐初始余额为1000元
MoneyBox moneyBox = new MoneyBox(1000);
System.out.println("-------存钱罐初始余额=" + moneyBox.getMoney() + "-------");
//保证张无忌和赵敏这两个线程操作的是同一个对象
//利用构造函数传参,把moneyBox存钱罐对象传入
Zhangwuji zhangwuji = new Zhangwuji("张无忌", moneyBox);
Zhaomin zhaomin = new Zhaomin("赵敏", moneyBox);
Thread t1 = new Thread(zhangwuji, "张无忌少侠");
Thread t2 = new Thread(zhaomin, "赵敏郡主");
//启动线程
t1.start();
t2.start();
//主线程睡眠4秒,以使其他两个线程先执行完
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印存钱罐中最终的余额
System.out.println(moneyBox);
}
}
运行结果如下:存钱罐中的初始额是1000元,张无忌存了10次钱,每次存100,而赵敏取了10次钱,每次取50,最后存钱罐中应该还剩1500元才对,可下面的运行结果却怎么也不对,可以多运行几次进行测试,每次运行结果都可能不一样,反正存钱罐中余额不等于1500,如果等于1500,那估计是巧合吧
接下来,看改进版
Zhangwuji2类
package com.demo.thread2;
public class Zhangwuji2 implements Runnable{
private String name;
private MoneyBox2 moneyBox2;
public Zhangwuji2(String name, MoneyBox2 moneyBox2){
this.name = name;
this.moneyBox2 = moneyBox2;
}
@Override
public void run() {
add();
}
public void add() {
for (int frequency = 1; frequency <= 10; frequency++) {
System.out.println("#######我是" + name + ",这是我第" + frequency + "次存钱#######");
moneyBox2.add(100, name, frequency);
}
}
}
Zhaomin2类
package com.demo.thread2;
public class Zhaomin2 implements Runnable {
private String name;
private MoneyBox2 moneyBox2;
public Zhaomin2(String name, MoneyBox2 moneyBox2) {
this.name = name;
this.moneyBox2 = moneyBox2;
}
@Override
public void run() {
get();
}
public void get() {
for (int frequency = 1; frequency <= 10; frequency++) {
System.out.println("*******我是" + name + ",这是我第" + frequency + "次取钱*******");
moneyBox2.get(50, name, frequency);
}
}
}
MoneyBox2类
package com.demo.thread2;
//存钱罐类
public class MoneyBox2 {
//存钱罐中的余额
private double money;
public double getMoney() {
return money;
}
// 构造函数 /构造方法/构造器
public MoneyBox2(double money) {
this.money = money;
}
/**
* 线程同步是为了保证当多个线程同时并发访问某资源时,在任何指定时间只有一个线程
* 可以访问该资源,从而使该资源的数据保持一致
* 其中同步又分为同步方法和同步块。我们先看同步方法,同步方法的实现很简单,只要
* 在方法的声明前面加上 synchronized 关键字就可以。它的原理是在任何时刻一个对象中只
有一个同步方法可以执行。只有对象当前执行的同步方法结束后,同一个对象的另一个同步
方法才可能开始。这里的思想是,保证每个线程在调用对象同步方法时以独占的方法操作该
对象。
线程同步的粒度越小越好,即线程同步的代码块越小越好。这时我们可以使用同步块。
因为同步块后面需要传递同步的对象参数,所以它可以指定更多的对象享受同步的优势,而
不像同步方法那样只有包含相关代码的对象受益。
*/
/**
* 同一时刻只能由一个同步方法执行,因为
* 该同步方法已经设置了锁,阻止了其他同步方法的启动
*/
/**
*
*synchronized(对象)同步块后面的小括号中,可以放任何对象,只要是引用类型(即对象类型)就可以,
*所以synchronized同步块比较灵活,没有局限性(同步块后面需要传递同
*步的对象,所以同步块的好处就是,可以指定更多的对象享受同步的优势),而不像同步方法那样只
*有包含相关代码的对象受益(即,同步方法写在哪个类中,哪个类的对象才能有同步的功能,所以同步方法相
*对于同步块来说,有局限性,所以,我个人建议大家使用同步块)
*/
//这是存钱的方法,加上synchronized(同步),写成同步方法,看下什么效果
public synchronized void add(double inMoney, String threadName, int number) {
double oldMoney = money;
try {
//拿到旧的金额后,让当前线程睡眠会以使别的线程操作它
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
money = oldMoney + inMoney;
System.out.println("-------" + threadName + "第" + number + "次存了" + inMoney + "元,现在存钱罐中的余额=" + money);
}
//这是取钱的方法,加上synchronized(同步),写成同步方法,看下什么效果
public synchronized void get(double outMoney, String threadName, int number) {
double oldMoney = money;
try {
//拿到旧的金额后,让当前线程睡眠会以使别的线程操作它
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
money = oldMoney - outMoney;
System.out.println("$$$$$$$" + threadName + "第" + number + "次取了" + outMoney + "元,现在存钱罐中的余额=" + money);
}
@Override
public String toString() {
return "---------------我是存钱罐,我现在的余额=" + money + "---------------";
}
}
Test2类
package com.demo.thread2;
public class Test2 {
//测试线程同步
public static void main(String[] args) {
//存钱罐初始余额为1000元
MoneyBox2 moneyBox2 = new MoneyBox2(1000);
System.out.println("-------存钱罐初始余额=" + moneyBox2.getMoney() + "-------");
//保证张无忌和赵敏这两个线程操作的是同一个对象
//利用构造函数传参,把moneyBox存钱罐对象传入
Zhangwuji2 zhangwuji2 = new Zhangwuji2("张无忌", moneyBox2);
Zhaomin2 zhaomin2 = new Zhaomin2("赵敏", moneyBox2);
Thread t1 = new Thread(zhangwuji2, "张无忌少侠");
Thread t2 = new Thread(zhaomin2, "赵敏郡主");
//启动线程
t1.start();
t2.start();
//主线程睡眠4秒,以使其他两个线程先执行完
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印存钱罐中最终的余额
System.out.println(moneyBox2);
}
}
运行结果如下:这结果就是正确的
接下来,我们再改进一下
Zhangwuji3类
package com.demo.thread2;
public class Zhangwuji3 implements Runnable{
private String name;
private MoneyBox3 moneyBox3;
public Zhangwuji3(String name, MoneyBox3 moneyBox3){
this.name = name;
this.moneyBox3 = moneyBox3;
}
@Override
public void run() {
add();
}
public void add() {
for (int frequency = 1; frequency <= 10; frequency++) {
System.out.println("#######我是" + name + ",这是我第" + frequency + "次存钱#######");
moneyBox3.add(100, name, frequency);
}
}
}
Zhaomin3类
package com.demo.thread2;
public class Zhaomin3 implements Runnable {
private String name;
private MoneyBox3 moneyBox3;
public Zhaomin3(String name, MoneyBox3 moneyBox3) {
this.name = name;
this.moneyBox3 = moneyBox3;
}
@Override
public void run() {
get();
}
public void get() {
for (int frequency = 1; frequency <= 10; frequency++) {
System.out.println("*******我是" + name + ",这是我第" + frequency + "次取钱*******");
moneyBox3.get(50, name, frequency);
}
}
}
MoneyBox3类
package com.demo.thread2;
//存钱罐类
public class MoneyBox3 {
//存钱罐中的余额
private double money;
public double getMoney() {
return money;
}
// 构造函数 /构造方法/构造器
public MoneyBox3(double money) {
this.money = money;
}
//这是存钱的方法,加上synchronized(同步)关键字,写成同步块,看下什么效果
public void add(double inMoney, String threadName, int number) {
/**
* 如果,我们add()函数写成synchronized同步方法,而不是写成synchronized同步块的话,
* 假设add()函数中有一段安全验证等等业务代码,而且这段安全验证等等业务代码执行挺长时间,而在执行这段安
* 全验证代码的这段时间内,其他线程又不能操作(本类/本对象)同一个MoneyBox3对象的其他同
* 步方法,要一直等add同步方法执行结束后,其他线程才能操作(本类/本对象)同一个MoneyBox3对
* 象的其他同步方法,显然,效率比较低
* 所以,建议使用synchronized同步块
*/
//安全验证等业务代码占用时间
try {
//拿到旧的金额后,让当前线程睡眠会以使别的线程操作它
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
//使用同步块,可以减小线程同步的粒度,线程同步的粒度越小越好,即线程同步的代码块越小越好
/*
* 同步代码块可以用更细粒度的控制锁,同步方法锁的范围比较大,而同步代码块范围要小点,一般同步的范围越
* 大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好
*/
synchronized(this){
// double oldMoney = money;
// money = oldMoney + inMoney;
// 可以将上面两句话,合二为一
money += inMoney;
System.out.println("-------" + threadName + "第" + number + "次存了" + inMoney + "元,现在存钱罐中的余额=" + money);
}
}
//这是取钱的方法,加上synchronized(同步)关键字,写成同步块,看下什么效果
public void get(double outMoney, String threadName, int number) {
/**
* 安全验证等业务代码占用时间
* 假设这里有一段安全验证等等业务代码,而且这段安全验证等等业务代码执行挺长时间
*
*/
try {
//拿到旧的金额后,让当前线程睡眠会以使别的线程操作它
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
//使用同步块,可以减小线程同步的粒度,线程同步的粒度越小越好,即线程同步的代码块越小越好
/*
* 同步代码块可以用更细粒度的控制锁,同步方法锁的范围比较大,而同步代码块范围要小点,一般同步的范围越
* 大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好
*/
synchronized(this){
// double oldMoney = money;
// money = oldMoney - outMoney;
// 可以 将上面两句话,合二为一
money -= outMoney;
System.out.println("$$$$$$$" + threadName + "第" + number + "次取了" + outMoney + "元,现在存钱罐中的余额=" + money);
}
}
@Override
public String toString() {
return "---------------我是存钱罐,我现在的余额=" + money + "---------------";
}
/*
* 小结:
* 同步方法直接在方法上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范
* 围比较大,而同步代码块范围要小点,一般同步的范围越大,性能就越差,一般需要加锁进行同步的时候,肯
* 定是范围越小越好,这样性能更好
*
* 同步代码块可以用更细粒度的控制锁
*
* 使用同步块,可以减小线程同步的粒度,线程同步的粒度越小越好,即线程同步的代码块越小越好
*/
}
Test3类
package com.demo.thread2;
public class Test3 {
//测试线程同步
public static void main(String[] args) {
//存钱罐初始余额为1000元
MoneyBox3 moneyBox3 = new MoneyBox3(1000);
System.out.println("-------存钱罐初始余额=" + moneyBox3.getMoney() + "-------");
//保证张无忌和赵敏这两个线程操作的是同一个对象
//利用构造函数传参,把moneyBox存钱罐对象传入
Zhangwuji3 zhangwuji3 = new Zhangwuji3("张无忌", moneyBox3);
Zhaomin3 zhaomin3 = new Zhaomin3("赵敏", moneyBox3);
Thread t1 = new Thread(zhangwuji3, "张无忌少侠");
Thread t2 = new Thread(zhaomin3, "赵敏郡主");
//启动线程
t1.start();
t2.start();
//主线程睡眠4秒,以使其他两个线程先执行完
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印存钱罐中最终的余额
System.out.println(moneyBox3);
}
}
运行结果如下:这结果也是正确的