java基础15-java多线程2 线程的几个重要概念

    科技2024-08-10  23

    线程的几个重要概念

    线程的几个重要概念1、线程同步01、同步方法02、同步代码块03、使用特殊域变量(volatile)实现线程同步04、使用重入锁实现线程同步05、使用局部变量实现线程同步 2、线程间通信01、使用 volatile 关键字(同线程同步中volatile 的使用)02、使用Object类的wait() 和 notify() 方法03、使用JUC工具类 CountDownLatch04、使用 ReentrantLock 结合 Condition05、基本LockSupport实现线程间的阻塞和唤醒 3、线程死锁死锁产生的条件死锁实例:如何避免死锁?死锁检测处理死锁的基本方法死锁恢复 4、线程控制:挂起、停止和恢复

    线程的几个重要概念

    在多线程编程时,你需要了解以下几个概念:

    线程同步 线程间通信 线程死锁 线程控制:挂起、停止和恢复

    1、线程同步

    为什么要线程同步?

    因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。

    01、同步方法

    即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。 注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。

    package day11.work1; //账户类 class Account { public Account(int money) { this.money = money; } int money;// 账户余额 // 取钱方法 // 第一种方式 同步方法 synchronized public synchronized void withdraw(String name, int mm) { if (mm > money) { System.out.println(name + "余额不足"); } else { try { Thread.sleep(1000);// 模拟取钱时间 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } money = money - mm; System.out.println(name + "取出" + mm + "元,余额" + money); } } } class Person extends Thread { String name;// 名字 Account ac;// 账户 public Person(String name, Account ac) { this.name = name; this.ac = ac; } @Override public void run() { ac.withdraw(name, 1000); } } public class Test4 { public static void main(String[] args) { Account account = new Account(1500); Person baba = new Person("爸爸", account); Person mama = new Person("妈妈", account); baba.start(); mama.start(); } } //输出 妈妈取出1000元,余额500 爸爸余额不足
    02、同步代码块

    即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。 注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

    package day11.work1; //账户类 class Account { public Account(int money) { this.money = money; } int money;// 账户余额 // 取钱方法 public synchronized void withdraw(String name, int mm) { if (mm > money) { System.out.println(name + "余额不足"); } else { try { Thread.sleep(1000);// 模拟取钱时间 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } money = money - mm; System.out.println(name + "取出" + mm + "元,余额" + money); } } } class Person extends Thread { String name;// 名字 Account ac;// 账户 public Person(String name, Account ac) { this.name = name; this.ac = ac; } @Override public void run() { synchronized (ac) {// 第二种解决方法 同步块锁定 ac.withdraw(name, 1000); } } } public class Test4 { public static void main(String[] args) { Account account = new Account(1500); Person baba = new Person("爸爸", account); Person mama = new Person("妈妈", account); baba.start(); mama.start(); } } 输出: 爸爸取出1000元,余额500 妈妈余额不足
    03、使用特殊域变量(volatile)实现线程同步

    (1)volatile关键字为域变量的访问提供了一种免锁机制; (2)使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新; (3)因此每次使用该域就要重新计算,而不是使用寄存器中的值; (4)volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

    package day11.work1; //账户类 class Account { public Account(int money) { this.money = money; } volatile int money;// 账户余额 第三中同步方法 volatile关键字 // 取钱方法 public synchronized void withdraw(String name, int mm) { if (mm > money) { System.out.println(name + "余额不足"); } else { try { Thread.sleep(1000);// 模拟取钱时间 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } money = money - mm; System.out.println(name + "取出" + mm + "元,余额" + money); } } } class Person extends Thread { String name;// 名字 Account ac;// 账户 public Person(String name, Account ac) { this.name = name; this.ac = ac; } @Override public void run() { ac.withdraw(name, 1000); } } public class Test4 { public static void main(String[] args) { Account account = new Account(1500); Person baba = new Person("爸爸", account); Person mama = new Person("妈妈", account); baba.start(); mama.start(); } }
    04、使用重入锁实现线程同步

    在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力。 ReenreantLock类的常用方法有: ReentrantLock() :创建一个ReentrantLock实例 lock() :获得锁 unlock() :释放锁 注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。 如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码 。如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。

    package day11.work1; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; //账户类 class Account { public Account(int money) { this.money = money; } int money; //需要声明这个锁 private Lock lock = new ReentrantLock(); //第三种同步方式 需要上锁解锁 // 取钱方法 public synchronized void withdraw(String name, int mm) { lock.lock();//上锁 if (mm > money) { System.out.println(name + "余额不足"); } else { try { Thread.sleep(1000);// 模拟取钱时间 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { lock.unlock();//解锁 } money = money - mm; System.out.println(name + "取出" + mm + "元,余额" + money); } } } class Person extends Thread { String name;// 名字 Account ac;// 账户 public Person(String name, Account ac) { this.name = name; this.ac = ac; } @Override public void run() { ac.withdraw(name, 1000); } } public class Test4 { public static void main(String[] args) { Account account = new Account(1500); Person baba = new Person("爸爸", account); Person mama = new Person("妈妈", account); baba.start(); mama.start(); } }
    05、使用局部变量实现线程同步

    如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。现在明白了吧,原来每个线程运行的都是一个副本,也就是说存钱和取钱是两个账户,只是名字相同而已。所以就会发生上面的效果。

    ThreadLocal与同步机制 a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题; b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式。

    package com.demo.test; /** * @author lixiaoxi * */ public class Bank { private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){ @Override protected Integer initialValue() { // TODO Auto-generated method stub return 0; } }; // 存钱 public void addMoney(int money) { count.set(count.get()+money); System.out.println(System.currentTimeMillis() + "存进:" + money); } // 取钱 public void subMoney(int money) { if (count.get() - money < 0) { System.out.println("余额不足"); return; } count.set(count.get()- money); System.out.println(+System.currentTimeMillis() + "取出:" + money); } // 查询 public void lookMoney() { System.out.println("账户余额:" + count.get()); } }

    2、线程间通信

    为什么要线程通信?

    多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。 2.当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失! 3.所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。

    什么是线程通信?

    多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。    于是我们引出了等待唤醒机制:(wait()、notify()) 就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify);

    首先,要短信线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析:

    01、使用 volatile 关键字(同线程同步中volatile 的使用)

    基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式。

    02、使用Object类的wait() 和 notify() 方法

    众所周知,Object类提供了线程间通信的方法:wait()、notify()、notifyaAl(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。

    注意: wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁

    public class TestSync { public static void main(String[] args) { // 定义一个锁对象 Object lock = new Object(); List<String> list = new ArrayList<>(); // 实现线程A Thread threadA = new Thread(() -> { synchronized (lock) { for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) lock.notify();// 唤醒B线程 } } }); // 实现线程B Thread threadB = new Thread(() -> { while (true) { synchronized (lock) { if (list.size() != 5) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("线程B收到通知,开始执行自己的业务..."); } } }); // 需要先启动线程B threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 再启动线程A threadA.start(); } }

    在线程A发出notify()唤醒通知之后,依然是走完了自己线程的业务之后,线程B才开始执行,这也正好说明了,notify()方法不释放锁,而wait()方法释放锁。

    03、使用JUC工具类 CountDownLatch

    jdk1.5之后在java.util.concurrent包下提供了很多并发编程相关的工具类,简化了我们的并发编程代码的书写,CountDownLatch基于AQS框架,相当于也是维护了一个线程间共享变量state

    package Test; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; public class Test10_7_7 { public static void main(String[] args) { CountDownLatch countDownLatch = new CountDownLatch(1); List<String> list = new ArrayList<>(); // 实现线程A Thread threadA = new Thread(() -> { for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) countDownLatch.countDown(); } }); // 实现线程B Thread threadB = new Thread(() -> { while (true) { if (list.size() != 5) { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("线程B收到通知,开始执行自己的业务..."); break; } }); // 需要先启动线程B threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 再启动线程A threadA.start(); } } 输出: 线程A向list中添加一个元素,此时list中的元素个数为:1 线程A向list中添加一个元素,此时list中的元素个数为:2 线程A向list中添加一个元素,此时list中的元素个数为:3 线程A向list中添加一个元素,此时list中的元素个数为:4 线程A向list中添加一个元素,此时list中的元素个数为:5 线程A向list中添加一个元素,此时list中的元素个数为:6 线程B收到通知,开始执行自己的业务... 线程A向list中添加一个元素,此时list中的元素个数为:7 线程A向list中添加一个元素,此时list中的元素个数为:8 线程A向list中添加一个元素,此时list中的元素个数为:9 线程A向list中添加一个元素,此时list中的元素个数为:10
    04、使用 ReentrantLock 结合 Condition
    package Test; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class Test10_7_7 { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); List<String> list = new ArrayList<>(); // 实现线程A Thread threadA = new Thread(() -> { lock.lock(); for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) condition.signal(); } lock.unlock(); }); // 实现线程B Thread threadB = new Thread(() -> { lock.lock(); if (list.size() != 5) { try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("线程B收到通知,开始执行自己的业务..."); lock.unlock(); }); threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } threadA.start(); } } 输出: 线程A向list中添加一个元素,此时list中的元素个数为:1 线程A向list中添加一个元素,此时list中的元素个数为:2 线程A向list中添加一个元素,此时list中的元素个数为:3 线程A向list中添加一个元素,此时list中的元素个数为:4 线程A向list中添加一个元素,此时list中的元素个数为:5 线程A向list中添加一个元素,此时list中的元素个数为:6 线程A向list中添加一个元素,此时list中的元素个数为:7 线程A向list中添加一个元素,此时list中的元素个数为:8 线程A向list中添加一个元素,此时list中的元素个数为:9 线程A向list中添加一个元素,此时list中的元素个数为:10 线程B收到通知,开始执行自己的业务...

    显然这种方式使用起来并不是很好,代码编写复杂,而且线程B在被A唤醒之后由于没有获取锁还是不能立即执行,也就是说,A在唤醒操作之后,并不释放锁。这种方法跟 Object 的 wait() 和 notify() 一样。

    05、基本LockSupport实现线程间的阻塞和唤醒

    LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。

    package Test; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.LockSupport; public class Test10_7_7 { public static void main(String[] args) { List<String> list = new ArrayList<>(); // 实现线程B final Thread threadB = new Thread(() -> { if (list.size() != 5) { LockSupport.park(); } System.out.println("线程B收到通知,开始执行自己的业务..."); }); // 实现线程A Thread threadA = new Thread(() -> { for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) LockSupport.unpark(threadB); } }); threadA.start(); threadB.start(); } } 输出: 线程A向list中添加一个元素,此时list中的元素个数为:1 线程A向list中添加一个元素,此时list中的元素个数为:2 线程A向list中添加一个元素,此时list中的元素个数为:3 线程A向list中添加一个元素,此时list中的元素个数为:4 线程A向list中添加一个元素,此时list中的元素个数为:5 线程A向list中添加一个元素,此时list中的元素个数为:6 线程B收到通知,开始执行自己的业务... 线程A向list中添加一个元素,此时list中的元素个数为:7 线程A向list中添加一个元素,此时list中的元素个数为:8 线程A向list中添加一个元素,此时list中的元素个数为:9 线程A向list中添加一个元素,此时list中的元素个数为:10

    3、线程死锁

    线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。

    死锁产生的条件

    互斥条件:一个资源,或者说一个锁只能被一个线程所占用,当一个线程首先获取到这个锁之后,在该线程释放这个锁之前,其它线程均是无法获取到这个锁的。 占有且等待:一个线程已经获取到一个锁,再获取另一个锁的过程中,即使获取不到也不会释放已经获得的锁。 不可剥夺条件:任何一个线程都无法强制获取别的线程已经占有的锁 循环等待条件:线程A拿着线程B的锁,线程B拿着线程A的锁。。

    死锁实例:
    package day11.work1; //死锁 class Keys{ public static String gold="金钥匙"; public static String silver="银钥匙"; } //锁1 class Lock1 implements Runnable{ @Override public void run() { try{ System.out.println("Lock1 running"); synchronized(Keys.gold) { System.out.println("Lock1 lock 金钥匙"); Thread.sleep(3000); synchronized(Keys.silver) { System.out.println("Lock1 lock 银钥匙"); System.out.println("恭喜LOCK1开门"); } } }catch (Exception e) { e.printStackTrace(); } } } //锁2 class Lock2 implements Runnable{ @Override public void run() { try{ System.out.println("Lock2 running"); synchronized(Keys.silver) { System.out.println("Lock2 lock 银钥匙"); Thread.sleep(3000); synchronized(Keys.gold) { System.out.println("Lock2 lock 金钥匙"); System.out.println("恭喜LOCK2开门"); } } }catch (Exception e) { e.printStackTrace(); } } } public class Test6 { public static void main(String[] args) { Lock1 l1=new Lock1(); Thread t1=new Thread(l1); t1.start(); Lock2 l2=new Lock2(); Thread t2=new Thread(l2); t2.start(); } } 输出: Lock1 running Lock2 running Lock1 lock 金钥匙 Lock2 lock 银钥匙

    这样便造成了死锁,Lock1拿着金钥匙,想要银钥匙,Lock2拿着银钥匙,想要金钥匙.

    如何避免死锁?

    加锁顺序:线程按照相同的顺序加锁。 加锁时限,线程获取锁的过程中限制一定的时间,如果给定时间内获取不到,就算了,别勉强自己。这需要用到Lock的一些API。

    死锁检测

    在发生死锁之后,程序就卡住了没有任何反应,但程序仍在运行,因此需要借助一些 首先使用 jps -l显示正在运行的虚拟机进程,并显示虚拟机执行主类(main函数所在 的类)名称以及这些进程的本地虚拟机唯一ID

    处理死锁的基本方法

    1.预防死锁:通过设置一些限制条件,去破坏产生死锁的必要条件

    2.避免死锁:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁

    3.检测死锁:允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉

    4.解除死锁:该方法与检测死锁配合使用

    死锁恢复

    4、线程控制:挂起、停止和恢复

    1、线程挂起、恢复 Thread 的API中包含两个被淘汰的方法,它们用于临时挂起suspend和重启唤醒resume某个线程,这些方法已经被淘汰,因为它们是不安全的,不稳定的。如果在不合适的时候挂起线程(比如,锁定共享资源时),此时便可能会发生死锁条件——其他线程在等待该线程释放锁,但该线程却被挂起了,便会发生死锁。另外,在长时间计算期间挂起线程也可能导致问题。

    2、终止线程 当调用Thread的start()方法,执行完run()方法后,或在run()方法中return,线程便会自然消亡。另外Thread API中包含了一个stop()方法,可以突然终止线程。但它在JDK1.2后便被淘汰了,因为它可能导致数据对象的崩溃。一个问题是,当线程终止时,很少有机会执行清理工作;另一个问题是,当在某个线程上调用stop()方法时,线程释放它当前持有的所有锁,持有这些锁必定有某种合适的理由——也许是阻止其他线程访问尚未处于一致性状态的数据,突然释放锁可能使某些对象中的数据处于不一致状态,而且不会出现数据可能崩溃的任何警告。 终止线程的替代方法:同样是使用标志位,通过控制标志位来终止线程。

    Processed: 0.011, SQL: 8