一、线程的死锁问题
- 不同的线程分别占用对方需要的同步资源不放,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
package com.csii.day03; /** * @Author wufq * @Date 2020/12/22 10:55 * 死锁演示 */ class A{ public synchronized void foo(B b){ System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入A实例的foo方法"); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用B实例的bar方法"); b.last(); } public synchronized void last(){ System.out.println("进入了A类的内部last方法"); } } class B{ public synchronized void bar(A a){ System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入B实例的bar方法"); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用A实例的foo方法"); a.last(); } public synchronized void last(){ System.out.println("进入了B类的内部last方法"); } } public class BeadLock implements Runnable{ A a = new A(); B b = new B(); public void init(){ Thread.currentThread().setName("主线程"); a.foo(b); System.out.println("进入主线程之后"); } public void run(){ Thread.currentThread().setName("副线程"); b.bar(a); System.out.println("进入副线程之后"); } public static void main(String[] args){ BeadLock bl = new BeadLock(); new Thread(bl).start(); bl.init(); } }
二、解决线程安全问题的方式三:lock锁 ---- JDK5.0新增
- 从JDK5.0开始,Java提供了更强大的线程同步机制---通过显式定义同步锁对象来实现同步。同步锁使用lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。
package com.csii.day03; import java.util.concurrent.locks.ReentrantLock; /** * @Author wufq * @Date 2020/12/22 15:14 * lock锁解决线程安全问题 */ class Window5 implements Runnable{ private int ticket = 100; private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true){ try { //调用lock()方法,获取同步监视器 lock.lock(); Thread.sleep(100); if(ticket>0){ System.out.println(Thread.currentThread().getName()+"票号:"+ticket); ticket --; }else { break; } } catch (InterruptedException e) { e.printStackTrace(); } finally { //调用解锁方法 lock.unlock(); } } } } public class LockTest { public static void main(String[] args){ Window5 w5=new Window5(); for(int i=0;i<3;i++) { Thread tt= new Thread(w5); tt.setName("窗口"+i+" "); tt.start(); } } }
synchronized与Lock的对比
1、Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
2、Lock只有代码块锁,synchronized有代码块锁和方法锁
3、使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
三、练习1
银行有一个账户
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
【提示】
1、明确哪些代码是多线程运行代码,须写入run()方法
2、明确什么是共享数据
3、明确多线程运行代码中哪些语句是操作共享数据的
package com.csii.day03; import java.util.concurrent.locks.ReentrantLock; /** * @Author wufq * @Date 2020/12/22 17:20 * 银行有一个账户 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。 分析: 是否涉及到多线程? 两个储户线程 是否有共享数据? 账户或者余额是共享数据 是否存在线程安全问题? 有线程安全问题 如何解决?(同步机制:三种方法) */ //账户类 class Account{ //定义账户 private double balance; private ReentrantLock lock = new ReentrantLock(); public Account(double balance) { this.balance = balance; } //amt是存的钱数 public void deposit(double amt) { try { //调用锁定方式lock() lock.lock(); if(amt>0){ balance += amt; Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance); } } catch (Exception e) { e.printStackTrace(); } finally { //调用解锁方式unlock() lock.unlock(); } } } //储户 class Customer implements Runnable{ private Account acc; public Customer(Account acc) { this.acc = acc; } @Override public void run() { for(int i=0;i<3;i++) { acc.deposit(1000); } } } public class AccountTest { public static void main(String[] args){ Account acc = new Account(0); Customer c1 = new Customer(acc); Customer c2 = new Customer(acc); Thread t1 = new Thread(c1); t1.setName("甲"); Thread t2 = new Thread(c2); t2.setName("乙"); t1.start(); t2.start(); } } ====执行结果==== 甲存钱成功,余额为:1000.0 甲存钱成功,余额为:2000.0 甲存钱成功,余额为:3000.0 乙存钱成功,余额为:4000.0 乙存钱成功,余额为:5000.0 乙存钱成功,余额为:6000.0
四、线程的通信
1、
涉及到的三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态
,并释放同步监视器
notify():一旦执行此方法,就会唤醒被wait的一个线程
如果多个线程被wait,就唤醒优先级高的那个
totifuAll():一旦执行此方法,就会唤醒被wait的所有线程
注意点:
1、wait(),notify(),notifyAll()三个方法
必须使用在同步代码块或同步方法中
2、wait(),notify(),notifyAll()三个方法的调用者必须
是同步代码块或者同步方法中的同步监视器,否则,
会出现IlllegalMonitorStateException异常
3、wait(),notify(),notifyAll()三个方法是定义在
java.lang.Object类中
package com.csii.day04; /** * @Author wufq * @Date 2020/12/24 10:35 * * 线程通信:使用两个线程打印1-100,线程1,线程2 交替打印 */ class Num implements Runnable{ private int number = 1; /* * 1、wait方法和notify方法其实前面是有this对象的,这就表明同步监视器,wait方法,notify方法是有被对象调用的,并且是谁调用对象就是谁 * 2、这里把同步监视器(锁)设置成obj的对象,而wait和notify方法依然采用this方法,执行后会报IllegalMonitorStateException异常 * 但是wait和notyfy方法都设置成obj对象,是可以运行成功的,这就表明:wait、notyfy、notyfyAll三个方法的调用必须是同步监视器内 * 方法,否则会报异常 * * */ Object obj = new Object(); @Override public void run() { // synchronized (this) { synchronized (obj) { while (true){ //一旦执行此方法,就会唤醒被wait的一个线程,如果多个线程被wait,就唤醒优先级高的那个 // this.notify(); obj.notify(); try { Thread.sleep(100); if(number<=100) { System.out.println(Thread.currentThread().getName() + ":" + number); number++; //一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器 // this.wait(); obj.wait(); }else { break; } } catch (Exception e) { e.printStackTrace(); } } } } } public class Communication { public static void main(String[] args){ Num num =new Num(); Thread t1 = new Thread(num); Thread t2 = new Thread(num); t1.setName("线程1:"); t2.setName("线程2:"); t1.start(); t2.start(); } } =====执行结果===== 线程1::1 线程2::2 线程1::3 线程2::4 线程1::5 线程2::6
2、sleep和wait方法的异同
相同点:一旦执行方法,都可以使的当前的线程进入阻塞状态
不同点:
1)、两个方法声明的位置不同:Thread类中声明sleep()方法,Object类中声明wait()方法
2)、调用的要求不同:sleep可以在任意需要的场景下调用,wait必须使用在同步代码块或者同步方法中
3)、关于释放同步监视器:如果两个方法都使用在同步代码块或者同步方法中,sleep()不释放,wait()会释放 --即一旦使用了wait必须使用notify进行唤醒被阻塞的线程
3、线程通信的金典例题:生产者和消费者的问题
生产者(productor)将产品交给店员(clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者视图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了在通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了在通知消费者来取走产品。
这里可能出现两个问题:
1)、生产者比消费者快时,消费者会漏掉一些数据没有收到
2)、消费者比生产者快时,消费者会取相同的数据
package com.csii.day04; /** * @Author wufq * @Date 2020/12/25 16:20 * */ //共享数据 class Clerk{ private int productCount = 0; //生产的产品 public synchronized void produceProduct() throws InterruptedException { if(productCount <20){ productCount++; System.out.println(Thread.currentThread().getName()+"开始生产第"+productCount+"个"); notify(); } else { wait(); } } //消费的产品 public synchronized void customerProduct() throws InterruptedException { if(productCount >0){ System.out.println(Thread.currentThread().getName()+"开始消费第"+productCount+"个"); productCount--; notify(); }else { wait(); } } } //生产者和消费者是两个多线程 class Producer implements Runnable{ private Clerk clerk; public Producer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println(Thread.currentThread().getName()+"开始生产产品..."); while (true){ try { Thread.sleep(100); clerk.produceProduct(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Customer implements Runnable{ private Clerk clerk; public Customer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println(Thread.currentThread().getName()+"开始消费产品..."); while (true){ try { Thread.sleep(200); clerk.customerProduct(); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class ProductTest { public static void main(String[] args){ Clerk clerk = new Clerk(); Producer p1 = new Producer(clerk); Customer c1 = new Customer(clerk); Thread t1 = new Thread(p1); Thread t2 = new Thread(c1); t1.setName("生产者"); t2.setName("消费者"); t1.start(); t2.start(); } }
五、JDK5.0新增线程创建方式
新增方式一:实现Callable接口
1、与使用Runable相比,Callable功能更强大些
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
2、Future接口
- 可以对具体Runable、Callable任务的执行结果进行取消,查询是否完成,获取结果等
- FutureTask是Future接口的唯一实现类
- FutureTask同时实现了Runable,Future接口。它即可以作为Runable被线程执行,又可以作为Future得到Callable的返回值
package com.csii.day04; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * @Author wufq * @Date 2020/12/28 11:29 */ //1、创建一个实现Callable接口的实现类 class NumTest implements Callable<Integer>{ // 2、实现call方法,将此线程需要执行的操作声明在call()中 /* * 实现20以内偶数的和 * */ @Override public Integer call() throws Exception { int sum = 0; for(int i=0;i<20;i++){ if(i%2==0){ System.out.println(i); sum += i ; } } return sum; } } public class Test001 { public static void main(String[] args){ // 3、创建Callable实现类的对象 NumTest numTest = new NumTest(); // 4、将此Callable实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象 FutureTask<Integer> task = new FutureTask<>(numTest); // 5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法 new Thread(task).start(); try { // 6、如果需要返回call方法的值,需要调用get方法来获取 Integer sum = task.get(); // get()方法返回值即为FutureTask构造器Callable
实现类重写call()的返回值 System.out.println("总和:"+sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } ====执行结果==== 0 2 4 6 8 10 12 14 16 18 总和:90
如何理解实现Callable接口的方式创建多线程比实现
Runable接口创建多线程方式强大?
1、call()可以有返回值
2、call() 可以跑出异常,被外面的操作铺货,
获取异常的信息
3、Callable是支持泛型的
新增方式二:使用线程池
|-- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
|-- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
|-- 好处:
1)提高响应速度(减少了创建新线程的时间)
2)降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3)便于线程管理:
corePoolSize:核心池的大小
maximumPoolSIze:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
4)JDK5.0提供了线程池相关API:ExecutorService和Executors
5)ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
--> void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runable
--> <T>Futuure<T>submit(Callable<T>task):执行任务,有返回值,一般用来执行Callable
--> void shutdown():关闭连接池
6)Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
--> Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
--> Executors.newFixedThreadPool(n);:创建一个可重用固定线程数的线程池(常用的一个普通的线程池)
--> Executors.newSingleThreadPool():创建一个只有一个线程的线程池
--> Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
package com.csii.day04; import java.util.concurrent.*; /** * @Author wufq * @Date 2020/12/28 16:33 * 创建线程的方式四:使用线程池 */ class NumberTest implements Runnable{ @Override public void run() { int sum = 0; for(int i=0;i<20;i++){ if(i%2 == 0){ System.out.println(Thread.currentThread().getName()+":"+i); sum += i; } } System.out.println("偶数和:"+sum); } } class NumberTest2 implements Runnable{ @Override public void run() { int sum = 0; for(int i=0;i<10;i++){ if(i%2 == 0){ System.out.println(Thread.currentThread().getName()+":"+i); sum += i; } } System.out.println("偶数和:"+sum); } } class NumberTest1 implements Callable<Integer>{ @Override public Integer call() throws Exception { int sum = 0; for(int i=0;i<20;i++){ if(i%2 != 0){ System.out.println(Thread.currentThread().getName()+":"+i); sum += i; } } return sum; } } public class ThreadPool { public static void main(String[] args){ // 1、提供指定线程数量的线程池 ExecutorService service = Executors.newFixedThreadPool(10); /* * 验证ExecutorService接口常用的子类 * */ System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor // 2、执行指定的线程的操作,需要提供实现Runable接口或Callable接口实现类的对象 service.execute(new NumberTest()); service.execute(new NumberTest2()); FutureTask<Integer> task = new FutureTask<>(new NumberTest1()); service.submit(task); try { /* * ExecutorService类的两个常用方法: * 1、execute用来执行Runable接口 -->没有返回值 * 2、submit用来执行Callable接口 -->有返回值 * */ Integer sum = task.get(); System.out.println("奇数和:"+sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } // 3、关闭连接池 service.shutdown(); } } =====执行结果====
class java.util.concurrent.ThreadPoolExecutor
pool-1-thread-1:0
pool-1-thread-1:2
pool-1-thread-1:4
pool-1-thread-1:6
pool-1-thread-1:8
pool-1-thread-1:10
pool-1-thread-1:12
pool-1-thread-1:14
pool-1-thread-1:16
pool-1-thread-1:18
偶数和:90
pool-1-thread-2:0
pool-1-thread-2:2
pool-1-thread-2:4
pool-1-thread-2:6
pool-1-thread-2:8
偶数和:20
pool-1-thread-3:1
pool-1-thread-3:3
pool-1-thread-3:5
pool-1-thread-3:7
pool-1-thread-3:9
pool-1-thread-3:11
pool-1-thread-3:13
pool-1-thread-3:15
pool-1-thread-3:17
pool-1-thread-3:19
奇数和:100
package com.csii.day04; import java.util.concurrent.*; /** * @Author wufq * @Date 2020/12/28 16:33 * 创建线程的方式四:使用线程池 */ class NumberTest implements Runnable{ @Override public void run() { int sum = 0; for(int i=0;i<20;i++){ if(i%2 == 0){ System.out.println(Thread.currentThread().getName()+":"+i); sum += i; } } System.out.println("偶数和:"+sum); } } class NumberTest2 implements Runnable{ @Override public void run() { int sum = 0; for(int i=0;i<10;i++){ if(i%2 == 0){ System.out.println(Thread.currentThread().getName()+":"+i); sum += i; } } System.out.println("偶数和:"+sum); } } class NumberTest1 implements Callable<Integer>{ @Override public Integer call() throws Exception { int sum = 0; for(int i=0;i<20;i++){ if(i%2 != 0){ System.out.println(Thread.currentThread().getName()+":"+i); sum += i; } } return sum; } } public class ThreadPool { public static void main(String[] args){ // 1、提供指定线程数量的线程池 ExecutorService service = Executors.newFixedThreadPool(2); /* * 验证ExecutorService接口常用的子类 * */ System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor // 2、执行指定的线程的操作,需要提供实现Runable接口或Callable接口实现类的对象 service.execute(new NumberTest()); service.execute(new NumberTest2()); FutureTask<Integer> task = new FutureTask<>(new NumberTest1()); service.submit(task); try { /* * ExecutorService类的两个常用方法: * 1、execute用来执行Runable接口 -->没有返回值 * 2、submit用来执行Callable接口 -->有返回值 * */ Integer sum = task.get(); System.out.println("奇数和:"+sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } // 3、关闭连接池 service.shutdown(); } } ====执行结果==== class java.util.concurrent.ThreadPoolExecutor pool-1-thread-1:0 pool-1-thread-1:2 pool-1-thread-1:4 pool-1-thread-1:6 pool-1-thread-1:8 pool-1-thread-1:10 pool-1-thread-1:12 pool-1-thread-1:14 pool-1-thread-1:16 pool-1-thread-1:18 偶数和:90 pool-1-thread-2:0 pool-1-thread-2:2 pool-1-thread-2:4 pool-1-thread-2:6 pool-1-thread-2:8 偶数和:20 pool-1-thread-1:1 pool-1-thread-1:3 pool-1-thread-1:5 pool-1-thread-1:7 pool-1-thread-1:9 pool-1-thread-1:11 pool-1-thread-1:13 pool-1-thread-1:15 pool-1-thread-1:17 pool-1-thread-1:19 奇数和:100
通过以上两段代码发现:
第一段代码:设置的线程连接池内的线程数大于实际运行线程数时,线程会按照实际的线程运行
第二段代码:设置的线程连接池内的线程数小于实际运行线程数时,超出的线程会重新按照线程1开始运行,映射到压测时,这也是为什么连接数少的时候,而又并发的线程数大的时候,请求少
导致TPS低的原因