写点什么

多线程详解第 4 讲:线程同步(重点)

作者:Java高工P7
  • 2021 年 11 月 11 日
  • 本文字数:4015 字

    阅读完需:约 13 分钟

import java.util.concurrent.ThreadLocalRandom;


//不安全的取钱


//两个人去银行取钱,账户


public clas


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


s UnsafeBank {


public static void main(String[] args) {


//账户


Account account = new Account(100,"结婚基金");


Drawing you = new Drawing(account,50,"你");


Drawing girlFriend = new Drawing(account,100,"girlFriend");


you.start();


girlFriend.start();


}


}


//账户


class Account{


int money; //余额


String name; //卡名


public Account(int money, String name) {


this.money = money;


this.name = name;


}


}


//银行:模拟取款


class Drawing extends Thread{


Account account; //账户


//取了多少钱


int drawingMoney;


//现在手里有多少钱


int nowMoney;


public Drawing(Account account, int drawingMoney,String name) {


super(name);


this.account = account;


this.drawingMoney = drawingMoney;


}


//取钱


@Override


public void run() {


//判断有没有钱


if(account.money - drawingMoney < 0){


System.out.println(Thread.currentThread().getName()+"钱不够,取不下");


return;


}


try {


Thread.sleep(1000);


} catch (InterruptedException e) {


e.printStackTrace();


}


//卡内余额 = 余额 - 你取的钱


account.money = account.money - drawingMoney;


//你手里的钱


nowMoney = nowMoney + drawingMoney;


System.out.println(account.name+"余额为:"+account.money);


System.out.println(this.getName()+"手里的钱"+nowMoney);


}


}


运行截图



3、线程不安全的集合


package com.kaung.syn;


import java.util.*;


import java.util.concurrent.ThreadPoolExecutor;


//线程不安全的集合


public class UnsafeList {


public static void main(String[] args) {


List<String> list = new ArrayList<>();


for (int i = 0; i < 10000; i++) {


new Thread(()->{


list.add(Thread.currentThread().getName());


}).start();


}


try {


Thread.sleep(3000);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(list.size());


}


}


运行截图



我们发现集合中的元素并没有到达 10000,少了 2 个。这是因为多个线程抢占同一个资源,就会出现多个线程添加到了同一个位置。


每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

4、同步方法

同步方法


  • 由于我们可以通过 private 关键字来保证数据对象只能被方法访问 , 所以我们只需要针对方法提出一套机制 , 这套机制就是 synchronized 关键字 , 它包括两种用法 : synchronized 方法 和 synchronized 块 .


同步方法 : public synchronized void method(int args) {}


  • synchronized 方法控制对 “对象” 的访问 , 每个对象对应一把锁 , 每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行 , 否则线程会阻塞 , 方法一旦执行 , 就独占该锁 , 直到该方法返回才释放锁 , 后面被阻塞的线程才能获得这个锁 , 继续执行。


缺陷 : 若将一个大的方法申明为 synchronized 将会影响效率


同步方法弊端


5、同步块

同步块


  • 同步块 : synchronized (Obj ) { }

  • Obj 称之为 同步监视器

  • Obj 可以是任何对象 , 但是推荐使用共享资源作为同步监视器

  • 同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是 this , 就是 这个对象本身 , 或者是 class [ 反射中讲解 ]

  • 同步监视器的执行过程


  1. 第一个线程访问 , 锁定同步监视器 , 执行其中代码 .

  2. 第二个线程访问 , 发现同步监视器被锁定 , 无法访问

  3. 第一个线程访问完毕 , 解锁同步监视器

  4. 第二个线程访问, 发现同步监视器没有锁 , 然后锁定并访问


对三大不安全案例的改造


1、安全的买票


package com.kaung.syn;


//不安全的买票


public class UnsafeBuyTicket {


public static void main(String[] args) {


BuyTicket station = new BuyTicket();


new Thread(station,"苦逼的我").start();


new Thread(station,"牛逼的你们").start();


new Thread(station,"可恶的黄牛党").start();


}


}


class BuyTicket implements Runnable{


//票


private int ticketNum = 10;


boolean flag = true; //外部停止标志


@Override


public void run() {


while(flag){


try {


buy();


} catch (InterruptedException e) {


e.printStackTrace();


}


}


}


// synchronized 同步方法,锁的是 this


private synchronized void buy() throws InterruptedException {


//判断是否有票


if(ticketNum <= 0){


flag = false;


return ;


}


//模拟延时


Thread.sleep(100);


//买票


System.out.println(Thread.currentThread().getName()+"拿到"+ticketNum--);


}


}


运行截图



2、安全的取钱


package com.kaung.syn;


import java.util.concurrent.ThreadLocalRandom;


//不安全的取钱


//两个人去银行取钱,账户


public class UnsafeBank {


public static void main(String[] args) {


//账户


Account account = new Account(100,"结婚基金");


Drawing you = new Drawing(account,50,"你");


Drawing girlFriend = new Drawing(account,100,"girlFriend");


you.start();


girlFriend.start();


}


}


//账户


class Account{


int money; //余额


String name; //卡名


public Account(int money, String name) {


this.money = money;


this.name = name;


}


}


//银行:模拟取款


class Drawing extends Thread{


Account account; //账户


//取了多少钱


int drawingMoney;


//现在手里有多少钱


int nowMoney;


public Drawing(Account account, int drawingMoney,String name) {


super(name);


this.account = account;


this.drawingMoney = drawingMoney;


}


//取钱


//synchronized 默认锁的是 this


@Override


public void run() {


//锁的对象是变化的量,需要增删改的对象


synchronized ( account){


//判断有没有钱


if(account.money - drawingMoney < 0){


System.out.println(Thread.currentThread().getName()+"钱不够,取不下");


return;


}


try {


Thread.sleep(1000);


} catch (InterruptedException e) {


e.printStackTrace();


}


//卡内余额 = 余额 - 你取的钱


account.money = account.money - drawingMoney;


//你手里的钱


nowMoney = nowMoney + drawingMoney;


System.out.println(account.name+"余额为:"+account.money);


System.out.println(this.getName()+"手里的钱"+nowMoney);


}


}


}


运行截图



3、线程安全的集合


package com.kaung.syn;


import java.util.*;


import java.util.concurrent.ThreadPoolExecutor;


//线程不安全的集合


public class UnsafeList {


public static void main(String[] args) {


List<String> list = new ArrayList<>();


for (int i = 0; i < 10000; i++) {


new Thread(()->{


synchronized (list){


list.add(Thread.currentThread().getName());


}


}).start();


}


//休眠主线程,避免主线程执行过快,run 线程还未执行完毕就打印结果。


try {


Thread.sleep(3000);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(list.size());


}


}


运行截图



总结


锁的对象是变化的量,需要增删改的对象。锁住大家都操作的共同资源,避免共同资源在同一时间被多个对象访问。

6、CopyOnWriteArrayList

线程安全的集合


package com.kaung.syn;


import java.util.concurrent.CopyOnWriteArrayList;


//测试 JUC 安全类型的集合


public class TestJUC {


public static void main(String[] args) {


CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();


for (int i = 0; i < 10000; i++) {


new Thread(()->{


list.add(Thread.currentThread().getName());


}).start();


}


try {


Thread.sleep(3_000);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(list.size());


}


}

7、死锁

死锁


多个线程各自占有一些共享资源 , 并且互相等待其他线程占有的资源才能运行 , 而导致两个或者多个线程都在等待对方释放资源 , 都停止执行的情形 . 某一个同步块同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 “ 死锁 ” 的问题 。


死锁避免方法


  • 产生死锁的四个必要条件:


  1. 互斥条件:一个资源每次只能被一个进程使用。

  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件 : 进程已获得的资源,在末使用完之前,不能强行剥夺。

  4. 循环等待条件 : 若干进程之间形成一种头尾相接的循环等待资源关系。


上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。


代码模拟死锁


package com.kuang.thread;


//死锁:多个线程互相抱着对方需要的资源,然后形成僵持


public class DeadLock {


public static void main(String[] args) {


Makeup g1 = new Makeup(0,"灰姑娘");


Makeup g2 = new Makeup(1,"白雪公主");


g1.start();


g2.start();


}


}


//口红


class Lipstick{


}


//镜子


class Mirror{


}


class Makeup extends Thread {


//需要的资源只有一份,用 static 来保证只要一份


static Lipstick lipstick = new Lipstick();


static Mirror mirror = new Mirror();


int choice; //选择


String girlName; //使用化妆品的人


Makeup(int choice, String girlName) {


this.choice = choice;


this.girlName = girlName;


}


@Override


public void run() {


//化妆


try {


makeup();


} catch (InterruptedException e) {


e.printStackTrace();


}


}


//化妆,互相持有对方的锁,就是需要拿到对方的资源


private void makeup() throws InterruptedException {


if (choice == 0) { //获得口红的锁


synchronized (lipstick) {


System.out.println(this.girlName + "获得口红的锁");


Thread.sleep(1000);


synchronized (mirror) {//一秒钟后想获得镜子


System.out.println(this.girlName + "获得镜子的锁");


}


}


} else {


synchronized (mirror) {


System.out.println(this.girlName + "获得镜子的锁");


Thread.sleep(2000);


synchronized (lipstick) {//一秒钟后想获得口红


System.out.println(this.girlName + "获得口红的锁");


}


}


}


}


}


程序运行卡死,互相僵持。


8、Lock(锁)

用户头像

Java高工P7

关注

还未添加个人签名 2021.11.08 加入

还未添加个人简介

评论

发布
暂无评论
多线程详解第4讲:线程同步(重点)