本文相关代码执行结果:https://mp.youkuaiyun.com/mp_blog/creation/editor?activity_id=10471&spm=1001.2014.3001.9461
一、.线程、进程和多线程
1.程序:说起线程不得不说程序,程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
2进程:进程是程序一次执行过程,他是一个动态的概念,是系统资源分配的单位;在操作系统中运行的程序就是进程,比如qq、播放器、游戏、IDE等
3.线程:一个进程可以有多个线程,但是一个线程至少也必须有一个线程,不然没有存在的意义,如视频里面中同时听声音,看图像,看弹幕等;线程是CPU调度和执行的单位;
注意:很多多线程是模拟出来的,真正的多线程是指多个CPU,即多核,如服务器。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
本节核心概念
□ 线程就是独立的执行路径
□ 在程序运行时,及时没有自己创建线程,后台也会有多个线程,如主线程,gc线程
□ main() 称为主线程,为系统的入口,用于执行整个程序
□ 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的
□ 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
□ 线程会带来额外的开销,如CPU调度时间,并发控制开销
□ 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
二、线程创建
三种创建方式
(1)***继承Thread类(Thread也是实现的Runnable接口)
□ 自定义线程类继承Threed
□ 重写run()方法,编写线程执行体
□ 创建线程对象,调用start()方法启动线程
public class TestThread extends Thread{ @Override public void run(){ // 线程体 for (int i = 0;i < 200;i++) { System.out.println("run方法体在执行:" + i); } } public static void main(String[] args) { // 创建线程对象 TestThread testThread = new TestThread(); // 调用start方法 testThread.start(); // main线程 for (int i = 0;i < 200;i++) { System.out.println("main方法体在执行:" + i); } } }
(2)***实现Runnable接口
□自定义线程类实现Runnable
□实现run()方法,编写线程执行体
□创建实现类对象,创建代理对象
□ 调用start()方法启动线程
public class TestRunnable implements Runnable{ @Override public void run() { // 线程体 for (int i = 0;i < 200;i++) { System.out.println("run方法体在执行:" + i); } } public static void main(String[] args) { // 创建实现类对象 TestRunnable testRunnable = new TestRunnable(); // 创建代理对象 Thread thread = new Thread(testRunnable); // 调用start方法 thread.start(); // main线程 for (int i = 0;i < 200;i++) { System.out.println("main方法体在执行:" + i); } } }
(3)实现Callable接口(了解)
好处:有返回值;可以抛出异常
缺点:代码复杂,需要通过服务进行提交;
本节小结
继承Thread类
□ 子类继承Thread类具备多线程能力
□ 启动线程:子类对象.start()
□ 不建议使用:避免OOP单继承局限性
实现Runnable
□ 实现Runnable接口具有多线程能力
□ 启动线程
□ 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
三、初始并发问题
1. 买火车票
没有模拟延时
// 多个线程同时操作一个对象 // 买火车票 public class BuyTicketsRunnable implements Runnable{ // 票数 private int ticketNums = 100; @Override public void run() { while (true) { if (ticketNums <= 0) { break; } System.out.println(Thread.currentThread().getName() + "-->拿到第" + ticketNums-- + "张票"); } } public static void main(String[] args) { BuyTicketsRunnable buyTicketsRunnable = new BuyTicketsRunnable(); new Thread(buyTicketsRunnable,"小明").start(); new Thread(buyTicketsRunnable,"小红").start(); new Thread(buyTicketsRunnable,"小蓝").start(); } }
有模拟延时
package com.zte.mds.web.test; // 多个线程同时操作一个对象 // 买火车票 public class BuyTicketsRunnable implements Runnable{ // 票数 private int ticketNums = 100; @Override public void run() { while (true) { if (ticketNums <= 0) { break; } // 模拟延时 try { Thread.sleep(200); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "-->拿到第" + ticketNums-- + "张票"); } } public static void main(String[] args) { BuyTicketsRunnable buyTicketsRunnable = new BuyTicketsRunnable(); new Thread(buyTicketsRunnable,"小明").start(); new Thread(buyTicketsRunnable,"小红").start(); new Thread(buyTicketsRunnable,"小蓝").start(); } }
发现问题:多个线程操作同一个资源时,线程不安全,数据紊乱。/n
2.龟兔赛跑
(1)首选是赛道距离,然后是要离终点越来越近
(2)判断比赛是否结束
(3)打印出胜利者
(4)龟兔赛跑开始
(5)故事中,乌龟赢得比赛,兔子需要睡觉,模拟兔子睡觉
(6)最后,乌龟赢得比赛
package com.zte.mds.web.test; // 模拟龟兔赛跑 public class TurtleRabbitRaceRunnable implements Runnable{ // 胜利者 private static String winner; @Override public void run() { for (int i = 0; i <= 100; i++) { // 模拟兔子睡觉 if (Thread.currentThread().getName().equals("兔子") && (i % 12 == 0)) { try { Thread.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 判断比赛是否结束 boolean flag = isGameOver(i); if (flag) { break; } System.out.println(Thread.currentThread().getName() + "--> 跑了" + i + "步"); } } // 判断是否完成比赛 private boolean isGameOver(int steps) { // 判断是否有胜利者 if (winner != null) { return true; }{ if (steps >= 100) { winner = Thread.currentThread().getName(); System.out.println("winner is" + winner); return true; } } return false; } public static void main(String[] args) { TurtleRabbitRaceRunnable runnable = new TurtleRabbitRaceRunnable(); new Thread(runnable, "兔子").start(); new Thread(runnable, "乌龟").start(); } }
四、静态代理模式
案例:
你:真实角色
婚庆公司:代理你,帮你处理结婚的事
结婚:实现结婚接口即可
// 结婚接口 interface Marry { void happyMarry(); }
// 你 真实角色,你去结婚 class You implements Marry { @Override public void happyMarry() { System.out.println("要结婚啦,超开心!"); } }
// 婚庆公司 代理角色,帮助你结婚 class WeddingCompany implements Marry { // 结婚对象 真实对象 private Marry target; public WeddingCompany(Marry target) { this.target = target; } @Override public void happyMarry() { before(); this.target.happyMarry(); after(); } private void before() { System.out.println("结婚前布置现场"); } private void after() { System.out.println("结婚之后收尾款"); } }
// 静态代理 public class StaticProxy { public static void main(String[] args) { You you = new You(); WeddingCompany weddingCompany = new WeddingCompany(you); weddingCompany.happyMarry(); } }
总结:真实对象和代理对象都要实现同一个接口;代理对象要代理真实角色;
好处:代理对象可以做很多真实对象做不了的事情,也可以说真实对象没有时间,没有必要做的事情;真实对象只需要专注做自己的事情即可;
new WeddingCompany(you).happyMarry(); new Thread(()-> System.out.println("我爱你")).start();
线程的底层实现就和这个静态代理很像,通过代理对象实现逻辑,都是实现Runnable接口;
五、Lamda表达式
□ 避免匿名内部类定义过多
□ 其实质属于函数式编程的概念
为什么要使用lamda表达式
□ 避免匿名内部类定义过多
□ 可以让代码看起来很简洁
□ 去掉了一堆没有意义的代码,只留下合下的逻辑
函数式接口(Function Interface)
□ 理解函数式接口是学习lamda表达式的关键
□ 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口
□ 对于函数式接口,我们可以通过lamda表达式来创建该接口的对象
// 函数式接口 public interface FunctionInterface { void iLike(); }
// 接口实现类 public class FunctionInterfaceImp implements FunctionInterface{ @Override public void iLike() { System.out.println("我是FunctionInterface的实现类,重写了它的抽象方法"); } }
// 主函数 public class FunctionInterfaceMain { // 静态内部类 static class FunctionInterfaceImpStatic implements FunctionInterface{ @Override public void iLike() { System.out.println("我是FunctionInterfaceImpStatic静态内部类"); } } public static void main(String[] args) { FunctionInterface iLike = new FunctionInterfaceImp(); // 1 iLike.iLike(); iLike = new FunctionInterfaceImpStatic(); // 2 iLike.iLike(); // 局部内部类 class FunctionInterfaceImpLocalInner implements FunctionInterface { @Override public void iLike() { System.out.println("我是FunctionInterfaceImpLocalInner局部内部类"); } } iLike = new FunctionInterfaceImpLocalInner(); // 3 iLike.iLike(); //匿名内部类 没有类的名称,必须借助接口或者父类 iLike = new FunctionInterface() { @Override public void iLike() { System.out.println("我是匿名内部类"); } }; // 4 iLike.iLike(); // 用lamda简化 ()是传递的参数 iLike = () -> { System.out.println("我是lamda表达式"); }; // iLike = () -> System.out.println("我是lamda表达式"); // 5 iLike.iLike(); } }
实现类、静态内部类、局部内部类、匿名内部类和lamda表达式是一个逐渐简化的过程
六、线程
1、线程状态
2、线程方法
setPriority(int newPriority) 更改线程的优先级
static void sleep(long mills) 在指定的毫秒数内让当前执行的线程休眠
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其它线程
void interrupt() 中断线程(不建议)
boolean isAlive() 测试线程是否处于活动状态
3、停止线程
□ 不建议使用JDK提供的stop(),destroy()
□ 推荐线程自己停止下来(利用次数,不建议死循环)
□ 建议使用一个标志位进行终止变量,当flag=false,则终止线程运行
// 测试stop public class StopThread implements Runnable{ //1.设置一个标志位 private boolean flag = true; @Override public void run() { int i = 0; while (flag) { System.out.println("run Thread" + i++); } } // 2.设置一个公开的方法停止线程 public void stop() { this.flag = false; } public static void main(String[] args) { StopThread stopThread = new StopThread(); new Thread(stopThread).start(); for (int i = 0;i < 1000; i++) { System.out.println("main" + i +"次"); if (i == 900) { // 调用自己写的stop方法让线程停止 stopThread.stop(); System.out.println("线程停止!"); } } } }
4、线程休眠
□ sleep(时间)指定当前线程阻塞的毫秒数
□ sleep存在异常InterruptedException
□ sleep时间到达后线程进入就绪状态
□ sleep可以模拟网络延时,倒计时等
// 模拟倒计时 public class AnalogCountdown { public static void temDown() throws InterruptedException { int num = 10; while (true) { Thread.sleep(1000); System.out.println(num--); if (num<=0) break; } } public static void main(String[] args) throws InterruptedException { temDown(); } }
public static void main(String[] args) throws InterruptedException { // temDown(); // 打印系统当前时间 Date date = new Date(System.currentTimeMillis()); // 获取系统当前时间 while (true) { Thread.sleep(1000); System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)); date = new Date(System.currentTimeMillis()); // 更新当前时间 } }
□ 每一个对象都有一个锁,sleep不会释放锁
5、线程礼让(yield)
□ 礼让线程,让当前正在执行的线程暂停,但不阻塞
□ 将线程从运行状态转为就绪状态
□ 让CPU重新调度,礼让不一定成功
// Runnable实现类,测试yield方法 class MyYield implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + "线程开始执行"); Thread.yield(); System.out.println(Thread.currentThread().getName() + "线程执行完成"); } }
// 主方法 public class YieldMain { public static void main(String[] args) { MyYield myYield1 = new MyYield(); MyYield myYield2 = new MyYield(); new Thread(myYield1,"线程1---").start(); new Thread(myYield2,"线程2---").start(); } }
输出说明:两次结果不同,说明线程礼让不一定成功,看CPU调度;礼让是暂停,不是重新执行;
6、join
□ join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
□ 可以想象成插队
package com.zte.mds.web.test.join; public class TestJoin implements Runnable{ @Override public void run() { for (int i = 0; i < 200; i++){ System.out.println("我是Vip线程,我不排队:" + i); } } public static void main(String[] args) throws InterruptedException { // 启动Vip线程 TestJoin testJoin = new TestJoin(); Thread thread = new Thread(testJoin); thread.start(); // 主线程 for (int i = 0; i < 1000; i++){ if (i == 20) { thread.join(); } System.out.println("我是main:" + i); } } }
7、线程状态观测
Thread.State
线程可以处于以下状态之一:
□ NEW
尚未启动的线程处于此状态
□ RUNNABLE
在Java虚拟机中执行的线程处于此状态
□ BLOCKED
被注释等待监视器锁定的线程处于此状态
□ WATTING
正在等待另一个线程执行特定动作的线程处于此状态
□ TIME_wATTING
正在等待另一个线程执行动作到指定等待时间的线程处于此状态
□TERMINATED
已退出的线程处于此状态
// 线程观察 public class ObserveState { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { for (int i = 0; i < 5; i++){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("---------"); }); // 观察启动前状态 Thread.State state = thread.getState(); System.out.println("启动前状态:" + state); //观察启动后状态 thread.start(); state = thread.getState(); System.out.println("启动后状态:" + state); // 启动后,只要线程不终止就一直输出状态 while (state != Thread.State.TERMINATED) { Thread.sleep(1000); state = thread.getState(); if (state == Thread.State.TERMINATED) { System.out.println("线程终止:" + state); } else { System.out.println("启动后,只要线程不终止就一直输出状态:" + state); } } } }
8、线程优先级
□ Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
□ 线程的优先级用数字表示,范围为1~10
□ Thread.MIN_PRIORITY = 1;
□ Thread.MAX_PRIORITY = 10;
□ Thread.NORM_PRIORITY = 5;
□ 使用一下方式改变或获取优先级
□ getPriority() setPriority(int xxx)
// 先设置优先级再执行 public class PriorityMain { public static void main(String[] args) throws Exception{ // 打印主线程优先级 System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority()); TestPriority testPriority1 = new TestPriority(); Thread thread1 = new Thread(testPriority1); Thread thread2 = new Thread(testPriority1); Thread thread3 = new Thread(testPriority1); Thread.sleep(1000); // 设置优先级再启动 thread2.setPriority(1); // 1 thread2.start(); thread1.start(); // 默认 thread3.setPriority(Thread.MAX_PRIORITY); // 10 thread3.start(); } }
线程优化级较高的线程不一定先执行。线程的执行顺序真正取决于CPU调度器(在Java中是JVM来调度),程序员无法控制。
9、守护线程
□ 线程分为用户线程和守护线程
□ 虚拟机必须确保用户线程执行完毕
□ 虚拟机不用等待守护线程执行完毕
□ 后台记录操作日志,监控内存,垃圾回收等都是守护线程
// 守护线程的target public class God implements Runnable{ @Override public void run() { while (true) { System.out.println("上帝保佑着你!"); } } }
// 用户线程的target public class YOu implements Runnable{ @Override public void run() { for (int i = 0; i < 36500; i++) { System.out.println("你一生都开心地活着"); } System.out.println("goodBye!"); } }
// 主代码 public class TestDaemon { public static void main(String[] args) { God god = new God(); YOu yOu = new YOu(); Thread thread = new Thread(god); thread.setDaemon(true); // 默认是false表示是用户线程,正常的线程都是用户线程 thread.start(); // 守护线程启动 new Thread(yOu).start(); // 用户线程启动 } }
程序结果分析:守护线程是没有结束的,用户线程结束了,程序就结束了,说明虚拟机不会等待守护线程执行结束;用户线程执行结束,虚拟机执行结束还有一段时间才会关闭,此时守护线程依旧还在执行; 守护线程和用户线程“并行”执行;
10、 sleep()、wait()、join()、yield()的区别
sleep()
sleep()方法可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,需要指定休眠时间,sleep()方法不会释放“锁标志”,如果有synchronized同步块,其他线程仍然不能访问共享数据。(Thread类方法)
wait()
wait()方法需要和notify()及notifyAll()两个方法一起搭配使用,必须在synchronized语句块内使用,也就是说,调用wait(),notify()和notifyAll()的任务在调用这些方法前必须拥有对象的锁,wait方法释放锁(Object类方法)
yield()
和sleep()方法类似,也不会释放“锁”,
区别在于:
1:sleep方法需要参数,而yield方法不需要参数。
2: sleep()方法给其他线程运行机会时不考虑其他线程的优先级,因此会给低优先级的线程运行的机会;yield()方法只会给相同优先级或更高优先级的线程运行的机会。
3:线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态。
4:sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明任何异常。
join()
join()方法执行后线程进入阻塞状态,例如A线程中调用了B线程的Join方法,那么线程A会进入到阻塞队列,直到线程B结束或者中断,join方法内部调用了wait方法,会释放锁
三、线程同步
多个线程操作同一个资源,处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入到对象的等待池,形成队列,等待前面线程使用完毕,下一个线程再使用;
□ 由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁就可。引起的性能问题:
□ 一个线程持有锁会导致其他所有需要此锁的线程挂起
□ 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
□ 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题
1、队列和锁
队列+锁才能保证线程的安全性
2、三大不安全案例
不安全的买票
// 买票的实现类 public class UnsafeBuyTicket implements Runnable{ // 票数 private int tickets = 10; // 外部停止标志 boolean flag = true; @Override public void run() { while (flag) { try { buy(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } private void buy() throws InterruptedException { // 判断是否有票 if (tickets <= 0) { flag = false; return; } //模拟延时 Thread.sleep(1000); // 买票 System.out.println(Thread.currentThread().getName() + "拿到" + tickets--); } }
// 主函数 public class UnsafeBuyMain { public static void main(String[] args) { UnsafeBuyTicket unsafeBuyTicket = new UnsafeBuyTicket(); new Thread(unsafeBuyTicket,"a").start(); new Thread(unsafeBuyTicket,"b").start(); new Thread(unsafeBuyTicket,"c").start(); } }
线程不安全,有负数
不安全的取钱
// 账户类 public class Account { int money; // 余额 String name; // 用户名 public Account(int money, String name) { this.money = money; this.name = name; } }
// 银行取钱:继承Thread public class UnsafeWithdrawMoney extends Thread{ Account account; // 取出的钱 int drawMoney; // 手里的钱 int nowMoney; public UnsafeWithdrawMoney (Account account, int drawMoney, String name) { super(name); this.account = account; this.drawMoney = drawMoney; } // 取钱 @Override public void run() { // 判断钱够不够 if (account.money - drawMoney < 0) { System.out.println(Thread.currentThread().getName() + "钱不够,取不了"); return; } try { Thread.sleep(1000); // sleep可以放大问题的发生性 } catch (InterruptedException e) { throw new RuntimeException(e); } // 卡内余额 account.money = account.money - drawMoney; // 你手里的钱 nowMoney = nowMoney + drawMoney; System.out.println(account.name + "余额为:" + account.money); System.out.println(Thread.currentThread().getName() + "手里的钱" + nowMoney); } }
// 主方法 public class UnsafeWithdrawMoneyMain { public static void main(String[] args) { // 账户 Account account = new Account(100,"结婚基金"); UnsafeWithdrawMoney you = new UnsafeWithdrawMoney(account,50,"你"); UnsafeWithdrawMoney girlFriend = new UnsafeWithdrawMoney(account,100,"女朋友"); you.start(); girlFriend.start(); } }
线程不安全的集合
// 不安全的集合 import java.util.ArrayList; import java.util.List; public class UnsafeList { public static void main(String[] args) throws InterruptedException { List<String> list = new ArrayList<>(); for (int i = 0; i < 10000; i++){ new Thread(()->{ list.add(Thread.currentThread().getName()); }).start(); } Thread.sleep(10); System.out.println(list.size()); } }
不安全原因:当多个线程在同时调用的时候,有两个线程选择了同一个i,都在向list里面添加元素,最后导致后面的把前面的覆盖掉了
3、同步方法
□ 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以,我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块.
同步方法:public synchronized void method(int args){}
□ synchronized方法控制对"对象"的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,知道该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为synchronized将会影响效率
□ 同步块:synchronized(Obj) { }
□ Obj称为同步监视器
□ Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
□ 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
□ 同步监视器的执行过程:
1.第一个线程访问,锁定同步监视器,执行其中的代码
2.第二个线程访问,发现同步监视器被锁定,无法访问
3.第一个线程访问完毕,解锁同步监视器
4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问
修改不安全的买票部分代码就可以让买票安全
//synchronized 同步方法,锁的是this private synchronized void buy() throws InterruptedException { // 判断是否有票 if (tickets <= 0) { flag = false; return; } //模拟延时 // Thread.sleep(1000); // 买票 System.out.println("----------------"); System.out.println(this); System.out.println("----------------"); System.out.println(Thread.currentThread().getName() + "拿到" + tickets--); }
上面这个synchronized锁的是this,this是UnsafeBuyTicket对象,ticket是UnsafeBuyTicket的属性
修改不安全的银行取钱,结果还是不正确
@Override public synchronized void run() { // 判断钱够不够 if (account.money - drawMoney < 0) { System.out.println(Thread.currentThread().getName() + "钱不够,取不了"); return; } try { Thread.sleep(1000); // sleep可以放大问题的发生性 } catch (InterruptedException e) { throw new RuntimeException(e); } // 卡内余额 account.money = account.money - drawMoney; // 你手里的钱 nowMoney = nowMoney + drawMoney; System.out.println("---------"); System.out.println(this); System.out.println("---------"); System.out.println(account.name + "余额为:" + account.money); System.out.println(Thread.currentThread().getName() + "手里的钱" + nowMoney); }
上面的synchronized锁的是下面这个对象,UnsafeWithdrawMoney,这个是银行对象,我们实际要锁的是Account对象,锁的是账户
应该如下修改
// 取钱 @Override public void run() { synchronized (account) { // 判断钱够不够 if (account.money - drawMoney < 0) { System.out.println(Thread.currentThread().getName() + "钱不够,取不了"); return; } try { Thread.sleep(1000); // sleep可以放大问题的发生性 } catch (InterruptedException e) { throw new RuntimeException(e); } // 卡内余额 account.money = account.money - drawMoney; // 你手里的钱 nowMoney = nowMoney + drawMoney; System.out.println("---------"); System.out.println(this); System.out.println("---------"); System.out.println(account.name + "余额为:" + account.money); System.out.println(Thread.currentThread().getName() + "手里的钱" + nowMoney); } }
锁的对象就是变化的量,需要增删改的对象
不安全的集合
package com.zte.mds.web.test.syn; import java.util.ArrayList; import java.util.List; public class UnsafeList { public static void main(String[] args) throws InterruptedException { List<String> list = new ArrayList<>(); for (int i = 0; i < 10000; i++){ new Thread(()->{ synchronized (list) { list.add(Thread.currentThread().getName()); } }).start(); } // Thread.sleep(10000); System.out.println(list.size()); } }
list锁了还是不安全,需要加sleep
4、死锁
□ 多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。某个同步代码块同时用有两个以上对象的锁时,就可能会发生死锁的问题
// 镜子 public class Mirror { }
// 口红 public class Lipstick { }
//两个女孩化妆 class MakeUp extends Thread{ public static void main(String[] args) { MakeUp g1 = new MakeUp(0, "111"); MakeUp g12 = new MakeUp(1, "1122221"); g1.start(); g12.start(); } // 需要的静态资源只有一份,用static来保证只有一份,很重要 static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror(); int choice; // 化妆选择 String name; // 化妆的人 public MakeUp(int choice, String name) { this.choice = choice; this.name = name; } @Override public void run() { // 化妆 try { makeup(); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 化妆,互相持有对方的锁,就是需要拿到对方的资源 private void makeup() throws InterruptedException { if (choice == 0) { synchronized (lipstick) { // 获得口红的锁 System.out.println(this.name + "获得口红锁"); Thread.sleep(1000); synchronized (mirror) { // 获得镜子的锁 System.out.println(this.name + "获得镜子的锁"); } } } else { synchronized (mirror) { // 获得镜子的锁 System.out.println(this.name + "获得镜子的锁"); Thread.sleep(2000); synchronized (lipstick) { // 获得口红锁 System.out.println(this.name + "获得口红锁"); } } } } }
卡死了,解决方法
// 化妆,互相持有对方的锁,就是需要拿到对方的资源 private void makeup() throws InterruptedException { if (choice == 0) { synchronized (lipstick) { // 获得口红的锁 System.out.println(this.name + "获得口红锁"); Thread.sleep(1000); } synchronized (mirror) { // 获得镜子的锁 System.out.println(this.name + "获得镜子的锁"); } } else { synchronized (mirror) { // 获得镜子的锁 System.out.println(this.name + "获得镜子的锁"); Thread.sleep(2000); } synchronized (lipstick) { // 获得口红锁 System.out.println(this.name + "获得口红锁"); } } }
5、死锁的必要条件
□ 产生死锁的必要条件
(1)互斥条件:一个资源每次只能被一个资源使用
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
(3)不剥夺条件:进程已获得资源,在未使用完之前,不能强行剥夺
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系;
6、Lock(锁)
□ 从JDK5.0开始,Java提供了强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
□ java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先红的Lock对象
□ ReentrantLock(可重入锁)类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实际线程安全的控制中,比较常用的是ReentranLock,可以显式加锁,释放锁
// 买票 实现Runnable class BuyTicketLockRunnable implements Runnable { int ticketNum = 10; // 定义lock锁 private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { try { lock.lock(); if (ticketNum > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(ticketNum--); } else { break; } } finally { lock.unlock(); } } }
// 主贷买 public class BuyTicketLock { public static void main(String[] args) { BuyTicketLockRunnable buyTicketLockRunnable = new BuyTicketLockRunnable(); new Thread(buyTicketLockRunnable).start(); new Thread(buyTicketLockRunnable).start(); new Thread(buyTicketLockRunnable).start(); } }
7、synchronized 和 Lock的对比
□ Lock是显式锁,需要手动开启和关闭锁,synchronized是隐式锁,出了作用域自动释放
□ Lock只有代码块锁,synchronized有代码块锁和方法锁
□ 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
□ 优先使用顺序
□Lock > 同步代码块(已经进入了方法体,分配了相应的资源)> 同步方法(在方法体之外)
四、线程协作
1、线程通信
□ Java提供了一下几个方法解决线程之间的通信问题
方法名 | 作用 |
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级高的线程优先调度 |
以上方法军事Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常
2、管程法
// 产品 public class Product { int id; // 产品编号 public Product(int id) { this.id = id; } }
// 生产者 public class Producer extends Thread{ Buffer buffer; public Producer(Buffer buffer) { this.buffer = buffer; } @Override public void run() { for (int i = 0; i < 100; i++){ buffer.push(new Product(i)); System.out.println("生产者生产了" + i + "只鸡"); } } }
// 消费者 public class Consumer extends Thread{ Buffer buffer; public Consumer(Buffer buffer) { this.buffer = buffer; } @Override public void run() { for (int i = 0; i < 100; i++){ System.out.println("消费者消费了" + buffer.pop().id + "只鸡"); } } }
// 缓冲区 public class Buffer { // 容器大小 Product[] products = new Product[10]; // 容器计数器 int count = 0; // 生产者放入产品 public synchronized void push(Product product) { // 如果容器满了就需要等待消费者消费 if (count == products.length) { // 通知消费者消费 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { // 没满放入产品 products[count] = product; count++; // 通知消费者消费 this.notifyAll(); } } // 消费者消费产品 public synchronized Product pop() { // 判断能否消费 if (count == 0) { //等待生产者生产 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 消费 count--; Product product = products[count]; this.notifyAll(); return product; } }
// 主方法 public class PlanmethodMain { public static void main(String[] args) { Buffer buffer = new Buffer(); new Producer(buffer).start(); new Consumer(buffer).start(); } }
3、使用线程池
□ 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
□ 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用,类似生活中的公共交通工具
□ 好处
□ 提高响应速度(减少了创建新线程的时间)
□ 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
□ 便于线程管理
□ corePoolSize():核心池的大小
□ maximumPoolSize():最大线程数
□ keepAliveTime():线程没有任务时最多保持多长时间会终止
□ JDK5.0起提供线程池相关API:ExecutorService和Executors
□ ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
□ void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
□ <T>Future<T>submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
□ void shutdown():关闭连接池
□ Executors:工具类、线程池的工厂嘞,用于创建并返回不同类型的线程池