1.1 线程、进程、多线程
1.1.1 普通方法调用和多线程
1.1.2 程序 进程 线程
一个进程可以有多个线程,比如视频中同时听到声音、看到图像和弹幕等等。
1.1.3 Process与Thread
说起进程,就不得不说下程序 。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
而进程 则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位。
通常在一个进程中可以包含若干个线程 ,当然一个进程中至少有一个线程,不然没有存在的意义。
线程 是CPU调度和执行的单位。
执行程序 -> 进程 -> 线程
1 注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在只有一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的“感觉”。
1.1.4 总结
线程就是独立的执行路径;
在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程(Java的垃圾回收机制);
main()称为主线程,是系统的入口,用于执行整个程序;
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序不能人为干预;
对同一份资源进行操作时,会存在资源抢夺的问题,需要加入并发控制;
线程会带来额外的开销,如cpu调度事件,并发控制开销;
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
1.2 线程创建
Thread 类->继承Thread类
Runnable 接口->实现Runnable接口(重点)
Callable 接口->实现Callable接口
1.2.1 Thread类
自定义线程类继承Thread类
重写run()方法,编写线程执行体
创建线程对象,调用start()方法启动线程
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.kibo24.demo1;public class TestThread extends Thread { @Override public void run () { for (int i = 0 ; i < 20 ; i++) { System.out.println("嘉然今天吃什么--" +i); } } public static void main (String[] args) { TestThread testThread1=new TestThread(); testThread1.start(); for (int i = 0 ; i < 2000 ; i++) { System.out.println("海子姐ybb--" +i); } } }
线程交替执行,每次执行结果不同
总结
1.2.1.1 【使用多线程下载图片】
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package com.kibo24.demo1;import org.apache.commons.io.FileUtils;import java.io.File;import java.io.IOException;import java.net.URL;public class TestThread2 extends Thread { private String url; private String name; public TestThread2 (String url,String name) { this .url=url; this .name=name; } @Override public void run () { WebDownloader wd=new WebDownloader(); wd.downloader(url,name); System.out.println("下载的文件名为" +name); } public static void main (String[] args) { TestThread2 testThread1=new TestThread2("https://kibo24-1305312055.cos.ap-beijing.myqcloud.com/QQ%E5%9B%BE%E7%89%8720210912143516.gif" ,"嘉然.gif" ); TestThread2 testThread2=new TestThread2("https://kibo24-1305312055.cos.ap-beijing.myqcloud.com/QQ%E5%9B%BE%E7%89%8720210912143700.jpg" ,"唐可可.gif" ); TestThread2 testThread3=new TestThread2("https://kibo24-1305312055.cos.ap-beijing.myqcloud.com/QQ%E5%9B%BE%E7%89%8720210912143743.jpg" ,"喜羊羊.gif" ); testThread1.run(); testThread2.run(); testThread3.run(); } } class WebDownloader { public void downloader (String url,String name) { try { FileUtils.copyURLToFile(new URL(url),new File(name)); } catch (IOException e) { System.out.println("IO异常,downloader方法出现异常" ); e.printStackTrace(); } } }
运行结果
运行结果不一定按顺序,由cpu调度安排
由于此次测试数量比较少,cpu可以按顺序安排线程
1.2.2 Runnable接口(重要)
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.kibo24.demo1;public class TestThread3 implements Runnable { @Override public void run () { for (int i = 0 ; i < 20 ; i++) { System.out.println("嘉然今天吃什么--" +i); } } public static void main (String[] args) { TestThread3 testThread3=new TestThread3(); Thread thread=new Thread(testThread3); thread.start(); new Thread(testThread3).start(); for (int i = 0 ; i < 2000 ; i++) { System.out.println("海子姐ybb--" +i); } } }
注意
创建“TestThread”类,implements Runnable;
在TestThread类中重写(因为此时不继承Thread,所以严格来说不算是重写)run()方法(即编写需要执行的代码);
创建TestThread类的对象和Thread的对象;
创建Thread类对象时把TestThread的对象放进其构造函数中;
通过Thread的对象启动线程;
或者
直接使用 new Thread(TestThread的对象).start() 开启线程;
运行结果
1.2.3 实现Callable接口(暂时不进行详解)
1.2.4 总结
继承Thread类
实现Runnable接口(重要)
实现Runnable接口,具有多线程能力;
启动线程:传入目标线程对象+Thread对象.start();
推荐使用:避免单继承局限性(接口相对于继承的优点),灵活方便,便于同一个对象被多个线程使用
1.2.3.1 【买火车票】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.kibo24.demo1;public class TestThread4 implements Runnable { private int ticketNum=10 ; @Override public void run () { while (true ) { if (ticketNum<=0 ) { break ; } System.out.println(Thread.currentThread().getName()+"买到了" +ticketNum--+"号票" ); } } public static void main (String[] args) { TestThread4 testThread4=new TestThread4(); new Thread(testThread4,"嘉然" ).start(); new Thread(testThread4,"七海" ).start(); new Thread(testThread4,"二十四" ).start(); } }
结果
每次执行的结果不同;
多个线程操作同一个资源的情况下可能会出现“两个人拿到同一张票”的错误(当然本次没有发生这种数据并发的问题)
1.2.3.2 【龟兔赛跑-并发问题】
情景设定
存在准确赛道长度;
判断比赛是否结束;
判断谁是胜者;
龟兔赛跑开始执行;
胜负结果以故事原文的乌龟获胜为准;
兔子会睡觉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 package com.kibo24.demo1;public class Race implements Runnable { private static String winner; @Override public void run () { int m=0 ; if (Thread.currentThread().getName().equals("兔子" )) { m=2 ; } else if (Thread.currentThread().getName().equals("乌龟" )) { m=1 ; } for (int i = 0 ; i <= 100 ; i+=m) { if (Thread.currentThread().getName().equals("兔子" )&& (i==50 )) { try { Thread.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } } boolean flag=gameOver(i); if (flag) { break ; } System.out.println(Thread.currentThread().getName()+"跑了" +i+"步" ); } } private boolean gameOver (int steps) { if (winner!=null ) { return true ; } else { if (steps>=100 ) { winner=Thread.currentThread().getName(); System.out.println(winner+"赢了!" ); return true ; } else { return false ; } } } public static void main (String[] args) { Race race=new Race(); new Thread(race,"兔子" ).start(); new Thread(race,"乌龟" ).start(); } }
结果
最后可能会出现赢了以后乌龟仍然向前跑的情况,这种情况较难避免,可能是因为虚拟机停止需要时间
1.3 静态代理模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class StaticPolicy { public static void main (String[] args) { You you=new You(); new Thread(()->System.out.println("嘉然你带我走吧" )).start(); new WeddingCompany(new You()).HappyMarry(); } } 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("结婚之后收尾款" ); } }
总结
真实对象和代理对象都要实现同一个接口(Marry/Runnable);
代理对象(WeddingCompany/Thread)要代理真实角色(You/TestThread);
代理对象可以做到许多真实对象做不到的事情;
真实对象专注于做自己的事情;
1.4 Lambda表达式
作用
避免匿名内部类定义过多;
可以让你的代码看起来很简洁;
去掉无意义的代码,保留核心逻辑。
1.4.1 Lambda表达式的演化过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 public class TestLambda1 { static class Like2 implements ILike { @Override public void Lambda () { System.out.println("ybbb" ); } } public static void main (String[] args) { ILike like=new Like(); like.Lambda(); like=new Like2(); like.Lambda(); class Like3 implements ILike { @Override public void Lambda () { System.out.println("ybbbb" ); } } like=new Like3(); like.Lambda(); like=new ILike() { @Override public void Lambda () { System.out.println("ybbbbb" ); } }; like=()-> System.out.println("ybbbbbbbbb" ); like.Lambda(); } } interface ILike { void Lambda () ; } class Like implements ILike { @Override public void Lambda () { System.out.println("ybb" ); } }
1.4.2 Lambda表达式的简化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class TestLambda22 { public static void main (String[] args) { ILove love=(int a)-> {System.out.println("ybb" +24 );}; love=(a)-> {System.out.println("ybb" +24 );}; love=(a)-> System.out.println("ybb" +24 ); love=a-> System.out.println("ybb" +24 ); love.Love(24 ); } } interface ILove { void Love (int a) ; }
1.5 线程状态
五大状态:
方法
说明
setPriority(int newPriority)
更改线程的优先级
static void sleep(long millis)
在指定的毫秒数内让当前正在执行的线程休眠
void join()
等待该线程终止
static void yield()
暂停当前正在执行的线程对象,并执行其他线程
void interrupt()
中断线程()
boolean isAlive()
测试线程是否处于活动状态
1.5.1 停止线程
不推荐使用JDK提供的stop()、destroy()等方法;
推荐线程自己终止;
建立使用一个标志位进行终止变量,当flag==false,线程终止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class TestStop implements Runnable { private boolean flag=true ; @Override public void run () { int i=0 ; while (flag) { System.out.println("线程正在运行--" +i++); } } public void stop () { this .flag=false ; } public static void main (String[] args) { TestStop testStop=new TestStop(); new Thread(testStop).start(); for (int i = 0 ; i < 1000 ; i++) { System.out.println("main线程--" +i); if (i==900 ) { testStop.stop(); System.out.println("线程停止了" ); } } } }
结果
一些问题
使用标志位不保证线程一定立刻停止,只是暂时不打印日志了而已;
当删掉代码“System.out.println(“main线程–”+i);”后,Thread线程也不输出数据,原因未知。
1.5.2 线程休眠
sleep()指定当前线程阻塞的毫秒数;
sleep存在异常InterruptedException;
sleep事件达到后线程进入就绪状态;
sleep可以模拟网络延时,倒计时等;
每个对象都有一个锁,sleep不会释放锁 。
1.5.2.1 模拟网络延时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class TestSleep implements Runnable { private int count=10 ; @Override public void run () { while (true ) { if (count<=0 ) { break ; } try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"买到了" +count--+"号票" ); } } public static void main (String[] args) { TestThread4 testThread4=new TestThread4(); new Thread(testThread4,"嘉然" ).start(); new Thread(testThread4,"七海" ).start(); new Thread(testThread4,"二十四" ).start(); } }
1.5.2.2 倒计时/获取系统时间
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import java.text.SimpleDateFormat;import java.util.Date;public class TestCountDown { public static void countDown () { int num=10 ; while (true ) { try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(num--); if (num<=0 )break ; } } public static void main (String[] args) { countDown(); Date stime=new Date(System.currentTimeMillis()); while (true ) { try { System.out.println(new SimpleDateFormat("HH:mm:ss" ).format(stime)); Thread.sleep(1000 ); stime=new Date(System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } }
1.5.3 线程礼让
礼让线程,让当前正在执行的线程暂停,但是不阻塞;
让线程从运行状态转为就绪状态;
让cpu重新调度,礼让不一定成功,全看cpu的意愿。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class TestYield { public static void main (String[] args) { MyYield myYield=new MyYield(); new Thread(myYield,"A" ).start(); new Thread(myYield,"B" ).start(); } } class MyYield implements Runnable { @Override public void run () { System.out.println(Thread.currentThread().getName()+"线程开始执行" ); Thread.yield(); System.out.println(Thread.currentThread().getName()+"线程停止执行" ); } }
结果
yield是让线程从运行状态转换到就绪状态,让其他线程执行(相当于让其他线程插队,自己则不阻塞队伍 ),cpu调度执行依然是随机的;
sleep是让线程从运行状态转为阻塞状态,阻塞结束后转为就绪状态
也就是说,在这个例子中,不管写不写yield(),都会出现4种情况
1.5.4 线程合并
Join合并线程,等待此线程执行完后,再执行其他线程,其他线程阻塞;
相当于强行插队,让自己先走完(并且会阻塞队伍,使其他线程无法执行 )。
代码
思路:先执行main线程,等到i达到200时执行vip线程并让其join(阻塞式插队),等待vip线程执行完毕后,main线程继续执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class TestJoin implements Runnable { @Override public void run () { for (int i = 0 ; i < 10 ; i++) { System.out.println("第" +i+"号vip来力" ); } } public static void main (String[] args) { TestJoin testJoin=new TestJoin(); Thread thread=new Thread(testJoin); for (int i = 0 ; i < 500 ; i++) { System.out.println("你main爷爷来了" +i); if (i==200 ) { thread.start(); try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
结果
1.5.5 线程状态观测
Thread.State
线程状态,可以处于以下状态之一:
一个线程可以在给定时间处于一个状态,这些状态是不反映任何操作系统线程状态的虚拟机状态
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import com.kibo24.demo1.TestThread;public class TestState { public static void main (String[] args) { Thread thread=new Thread(()->{ for (int i = 1 ; i <= 5 ; i++) { System.out.println("ybb" ); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread.State state=thread.getState(); System.out.println(state); thread.start(); state=thread.getState(); System.out.println(state); while (state!= Thread.State.TERMINATED) { try { Thread.sleep(300 ); } catch (InterruptedException e) { e.printStackTrace(); } state=thread.getState(); System.out.println(state); } } }
结果
1.5.6 线程的优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
线程优先级用数字来表示,范围[1,10]。
Thread.MIN_PRIORITY=1
Thread.MAX_PRIORITY=10
Thread.NORM_PRIORITY=5
使用以下方式改变优先级
getPriority(). setPriority (int priority)
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class TestPriority { public static void main (String[] args) { System.out.println(Thread.currentThread().getName()+"优先级:" +Thread.currentThread().getPriority()); Priority priority=new Priority(); Thread t1=new Thread(priority,"1" ); Thread t2=new Thread(priority,"2" ); Thread t3=new Thread(priority,"3" ); Thread t4=new Thread(priority,"4" ); Thread t5=new Thread(priority,"5" ); Thread t6=new Thread(priority,"6" ); t1.start(); t2.setPriority(2 ); t2.start(); t3.setPriority(4 ); t3.start(); t4.setPriority(7 ); t4.start(); t5.setPriority(1 ); t5.start(); t6.setPriority(10 ); t6.start(); } } class Priority implements Runnable { @Override public void run () { System.out.println(Thread.currentThread().getName()+"优先级:" +Thread.currentThread().getPriority()); } }
结果
优先级高的线程不一定就会跑在优先级低的线程前面;
自己设置的优先级相当于“一个建议 ”,最终依然要服从cpu调度;
当有网络延迟的时候优先级的作用会更好的体现。
1.5.7 守护线程
线程分为用户线程 和守护线程 ;
虚拟机必须确保用户线程(如main线程)执行完毕;
虚拟机不用等待守护线程(gc线程)执行完毕;
例如后台记录操作日志,监控内存,垃圾回收等。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class TestDaemon { public static void main (String[] args) { Diane diane=new Diane(); You you=new You(); Thread thread=new Thread(diane); thread.setDaemon(true ); thread.start(); new Thread(you).start(); } } class Diane implements Runnable { @Override public void run () { while (true ) { System.out.println("嘉然守护着你" ); } } } class You implements Runnable { @Override public void run () { for (int i = 0 ; i < 3650 ; i++) { System.out.println("第" +i+"天,你活的好好的" ); } System.out.println("你终于死了,准备重开吧" ); } }
结果
在用户线程达到3649,即执行完毕后,虚拟机停止;
守护线程(嘉然)在用户线程结束后依然跑了一会儿,是因为虚拟机停止需要时间。
1.6 线程同步机制
1.6.1 并发
1.6.2 线程同步
处理多线程问题时,多个线程访问同一个对象(并发问题),并且某些线程还想要修改这个对象,这时候我们就需要线程同步。
线程同步其实就是一个等待机制,多个需要同时访问此对象的线程进入这个对象等待池 形成队列,等待前面线程使用完毕,下一个线程再使用。
1.6.3 队列和锁
由于同一进程的多个线程共享同一块存储空间,存在访问冲突的问题,为了保证数据在方法中被访问的正确性,在访问时加入锁机制 synchronized ,当一个线程获得对象的排他锁,就独占资源,其他线程必须等待,使用后释放锁即可继续执行 ,锁机制存在的问题:
一个线程持有锁会导致其他所有需要此锁的线程挂起;
在多线程竞争下,加锁或释放锁会导致比较多的上下文切换和调度延时,引发性能问题;
如果一个高优先级线程等待一个低优先级线程释放锁,会导致优先级倒置,引发性能问题。
1.6.3.1 【不安全的买票】
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class UnsafeBuyTickets { public static void main (String[] args) { BuyTickets buyTickets=new BuyTickets(); new Thread(buyTickets,"嘉然" ).start(); new Thread(buyTickets,"七海" ).start(); new Thread(buyTickets,"二十四" ).start(); } } class BuyTickets implements Runnable { private int count=0 ; boolean flag=true ; @Override public void run () { while (flag) { buy(); } } private void buy () { if (count>=10 )flag=false ; System.out.println(Thread.currentThread().getName()+"买到了第" +(count++)+"号票" ); } }
结果
因为没有线程同步(排队),所以有可能出现多个线程争抢同一份资源引发的错误。
1.6.3.2 【不安全的取款】
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public class UnsafeBank { public static void main (String[] args) { Account account=new Account(); account.money=114514 ; account.name="七海的礼物" ; Bank bank=new Bank(account,50000 ,"嘉然" ); Bank bank2=new Bank(account,70000 ,"七海" ); bank.start(); bank2.start(); } } class Account { int money; String name; } class Bank extends Thread { Account account; int drawingMoney; int nowMoney=0 ; public Bank (Account account, int drawingMoney, String name) { super (name); this .account = account; this .drawingMoney = drawingMoney; } @Override public void run () { if (account.money<drawingMoney) { System.out.println(Thread.currentThread().getName()+"想取" +drawingMoney+",但是没钱了" ); return ; } try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } account.money-=drawingMoney; nowMoney+=drawingMoney; System.out.println(Thread.currentThread().getName()+"取走了" +drawingMoney+",还剩" +account.money+",手里现在有" +nowMoney); } }
结果
两个线程都先判断余额足够,然后等待一下开始取钱(所以代码中的if判断余额的机制拦不住两个线程)
使用队列和锁让他们排队可以解决问题。
1.6.3.3 【不安全的List】
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.util.ArrayList;import java.util.List;public class UnsafeList { public static void main (String[] args) { List<String> list=new ArrayList<String>(); for (int i = 1 ; i <= 10000 ; i++) { new Thread(()->{ list.add(Thread.currentThread().getName()); }).start(); } System.out.println("list的大小是(理想是10000):" +list.size()); } }
结果
1.7 同步方法
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它有两种用法:synchronized方法 和synchronized方法块 。
1 同步方法:public synchronized void method (int args) {}
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线层会阻塞,方法一旦执行,就独占该锁,知道该方法返回才释放锁,后面被阻塞的线程才会获得这个锁,继续执行;
1 缺点:将一个大的方法申明为synchronized方法会影响效率
1.7.1 同步方法的弊端
方法里面需要修改的内容才需要锁,锁的太多会浪费资源;
1.7.2 同步块
同步块:
Obj称为同步监视器 :
Obj可以是任何对象,但是推荐使用共享资源 作为同步监视器;
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,即这个对象本身,或者是class
同步监视器的执行过程:
第一个线程访问,锁定同步监视器,执行其中的代码;
第二个线程访问,发现同步监视器被锁定,无法访问;
第一个线程访问完毕,解锁同步监视器;
第二个线程访问,发现同步监视器没有锁,进行锁定并开始执行代码。
1.7.2.1 【经过修改的取钱代码】
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public class UnsafeBank { public static void main (String[] args) { Account account=new Account(); account.money=114514 ; account.name="七海的礼物" ; Bank bank=new Bank(account,50000 ,"嘉然" ); Bank bank2=new Bank(account,70000 ,"七海" ); bank.start(); bank2.start(); } } class Account { int money; String name; } class Bank extends Thread { Account account; int drawingMoney; int nowMoney=0 ; public Bank (Account account, int drawingMoney, String name) { super (name); this .account = account; this .drawingMoney = drawingMoney; } @Override public void run () { synchronized (account) { if (account.money<drawingMoney) { System.out.println(Thread.currentThread().getName()+"想取" +drawingMoney+",但是没钱了" ); return ; } try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } account.money-=drawingMoney; nowMoney+=drawingMoney; System.out.println(Thread.currentThread().getName()+"取走了" +drawingMoney+",还剩" +account.money+",手里现在有" +nowMoney); } } }
将run方法用synchronized锁住;
但是因为run中要修改的是account而不是默认的额this(Bank的对象),所以要使用代码块synchronized(account) ,而不能直接在方法前加上synchronized;
锁的对象就是**变化(增删改)**的对象。
结果
给我排队取钱(锁住之后的排队顺序取决于线程启动(start)的顺序,即先来后到 )
1.7.2.2 【经过修改的List】
代码
在Lambda表达式中加入了同步块
在输出结果之前
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import java.util.ArrayList;import java.util.List;public class UnsafeList { public static void main (String[] args) { List<String> list=new ArrayList<String>(); for (int i = 1 ; i <= 10000 ; i++) { new Thread(()->{ synchronized (list) { list.add(Thread.currentThread().getName()); } }).start(); } try { Thread.sleep(300 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("list的大小是(理想是10000):" +list.size()); } }
结果
上面的代码中加了Thread.sleep(),如果不加的话,可能会出现大小仍然小于10000的情况;
原因分析:for循环执行很快,add执行也很快,但是等for循环结束之后,还有一些线程没有跑完,这时候延迟显示结果就是在等待所有线程结束;
不同的是,不加synchronized之前减少的原因是多个线程争抢同一块资源+线程没跑完之前就显示list大小 ,而加上synchronized之后的原因就只有线程没跑完之前就显示list大小 这一点了;
所以如果sleep时间过短,理论上显示的数值也有可能小于10000。
这两种结果的区别:
多个线程争抢同一块资源+线程没跑完之前就显示list大小
最终结果,list的大小也不一定会到达10000,因为存在多抢一的操作;
线程没跑完之前就显示list大小
最终结果,虽然不加sleep也会显示不到10000,但是最终list的大小实际上是达到10000了的,多sleep一会儿必定能显示list大小为10000。
1.7.3 JUC安全集合
代码
java.util.concurrent包,是一个安全的包,相当于加上了synchronized
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import java.util.concurrent.CopyOnWriteArrayList;public class TestJUC { public static void main (String[] args) { CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList(); for (int i = 0 ; i < 10000 ; i++) { new Thread(()->{ copyOnWriteArrayList.add(Thread.currentThread().getName()); }).start(); } System.out.println("list的大小是:" +copyOnWriteArrayList.size()); try { Thread.sleep(300 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("list的大小是:" +copyOnWriteArrayList.size()); } }
结果
具体原因上面分析过了
1.7.4 死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或以上的线程都在等待对方释放资源而停止执行的情况,某一个同步块同时拥有两个或以上个对象的锁 时,就可能会发生死锁的问题。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 public class DeadLock { public static void main (String[] args) { Kibo24 k1=new Kibo24(0 ,"二十四" ); Kibo24 k2=new Kibo24(1 ,"王富贵" ); new Thread(k1).start(); new Thread(k2).start(); } } class Jiaran {}class Nanami {}class Kibo24 implements Runnable { static Jiaran dIane=new Jiaran(); static Nanami nanami=new Nanami(); int choice; String name; public Kibo24 (int choice, String name) { this .choice = choice; this .name = name; } @Override public void run () { rob(); } public void rob () { if (choice==0 ) { synchronized (dIane) { System.out.println(this .name+"要把嘉然抢走了!" ); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this .name+"还想要抢走七海!" ); synchronized (nanami) { System.out.println(this .name+"要把七海抢走了!" ); } } } else { synchronized (nanami) { System.out.println(this .name+"要把七海抢走了!" ); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this .name+"还想要抢走嘉然!" ); synchronized (dIane) { System.out.println(this .name+"要把嘉然抢走了!" ); } } } } }
结果
1.7.4.1 总结
产生死锁的必要条件:
互斥 :一个资源每次只能被一个进程使用;
请求与保持条件 :一个进程因请求资源而阻塞时,对已经获得的资源保持不放;
不剥夺条件 :进程已经获得的资源,在未使用完之前,不能强行剥夺;
循环等待条件 :若干进程之间形成一种头尾相接的循环等待资源的关系。
只要避免一个或多个条件就可以避免死锁的发生
1.7.5 Lock锁
从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当;
Lock接口是控制多个线程对资源共享进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象;
ReentrantLock(可重入锁 )类实现了Lock,他拥有和synchrd相同的并发性和内存语义,在实现线程安全的控制中,比较常用的私ReentrantLock,可以显式加锁,释放锁。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import java.util.concurrent.locks.ReentrantLock;public class TestLock { public static void main (String[] args) { TestLocker t1=new TestLocker(); new Thread(t1,"嘉然" ).start(); new Thread(t1,"七海" ).start(); new Thread(t1,"二十四" ).start(); } } class TestLocker implements Runnable { private int count=0 ; private ReentrantLock lock=new ReentrantLock(); @Override public void run () { while (true ) { try { Thread.sleep(300 ); } catch (InterruptedException e) { e.printStackTrace(); } try { lock.lock(); if (count<=10 ) { System.out.println(Thread.currentThread().getName()+"抢到了第" +count+++"号票" ); } else { break ; } } finally { lock.unlock(); } } } }
注意sleep的位置,如果在lock()内,就会只连续地执行一个或几个线程;如果在lock()外,多个线程才会基本全部执行;
原因分析:这是一个先等再锁 还是先锁再等 的问题。
先等再锁:在线程上锁之前的等待过程中,任何一个线程率先执行都是有可能的
先锁再等:由于调度很迅速,于是第一个启动的线程马上被上锁,在其解锁后依旧马上被上锁,使其他线程阻塞,其他线程很难执行(并不是不可能执行)
综上所述,建议先等再锁 。
结果
1.7.5.1 总结
Lock是显式锁,手动开启和关闭,synchronized是隐式锁,执行出了作用域自动释放;
Lock只有代码块锁,synchrond有代码块锁和方法锁;
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有良好的扩展性(有更多的子类);
优先使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步资源(在方法体之外)
1.8 线程协作
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
对于生产者,没有生产产品之前,要通知消费者等待;在生产了产品之后,又需要马上通知消费者;
对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费;
在生产消费者问题中,仅有synchronized是不够的
synchronized可阻止并发更新用一个资源共享,实现了同步;
synchronized不能用来实现不同线程之间的消息传递(通信)。
1.8.1 线程通信
Java提供了几个方法解决线程之间的通信问题:
方法名
作用
wait()
表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
wait(long time)
指定等待的时间
notify()
唤醒一个处于等待状态的线程
notifyAll()
唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度
均是Object类的方法,只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMonitorStateException
1.8.1.1 管程法
并发协作模型“生产者/消费者”—>管程法
生产者:负责产生数据的模块(可能是方法,对象,线程,进程)
消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
缓冲区:消费者不能直接使用生产者数据,生产者将数据放入缓冲区,消费者从缓冲区拿出数据
notify和notifyAll
使用notify()时,ThreadScheduler将从等待该监视器上的线程的池中选择一个随机线程,这取决于线程调度器,但是会保证只有一个线程会被通知:(随机性)
notifyAll方法将唤醒所有线程 。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 public class TestPC { public static void main (String[] args) { Container container=new Container(); Productor productor=new Productor(container); Consumer consumer=new Consumer(container); new Thread(productor).start(); new Thread(consumer).start(); } } class Productor implements Runnable { private Container container; public Productor (Container container) { this .container=container; } @Override public void run () { for (int i = 1 ; i <= 100 ; i++) { container.push(new Product(i)); } } } class Consumer implements Runnable { private Container container; public Consumer (Container container) { this .container=container; } @Override public void run () { for (int i = 1 ; i <= 100 ; i++) { container.pop(new Product(i)); } } } class Product { private int id; public Product (int id) { this .id=id; } public int getId () { return this .id; } } class Container { Product[] products=new Product[10 ]; private int count=0 ; public synchronized void push (Product product) { while (count==products.length) { try { this .wait(); } catch (InterruptedException e) { e.printStackTrace(); } } products[count++]=product; System.out.println("生产者生产了第" +product.getId()+"个产品" ); this .notifyAll(); } public synchronized Product pop (Product product) { Product finalProduct=null ; while (count==0 ) { try { this .wait(); } catch (InterruptedException e) { e.printStackTrace(); } } finalProduct=products[--count]; System.out.println("消费了第" +product.getId()+"个产品" ); this .notifyAll(); return finalProduct; } }
注意使用while循环判断条件,使用if可能会出现假唤醒问题
结果
1.8.1.2 信号灯法
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 public class TestPCTwo { public static void main (String[] args) { PlayStation playStation=new PlayStation("Apex" ); Player player=new Player(playStation); Fixer fixer=new Fixer(playStation); new Thread(player).start(); new Thread(fixer).start(); } } class Player implements Runnable { private PlayStation playStatio; public Player (PlayStation playStatio) { this .playStatio = playStatio; } @Override public void run () { for (int i = 1 ; i <= 20 ; i++) { this .playStatio.Play(); } } } class Fixer implements Runnable { private PlayStation playStatio; public Fixer (PlayStation playStatio) { this .playStatio = playStatio; } @Override public void run () { for (int i = 1 ; i <= 20 ; i++) { this .playStatio.fix(); } } } class PlayStation { private String game; private boolean flag=true ; public PlayStation (String game) { this .game = game; } public synchronized void Play () { while (flag == false ) { try { this .wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("七海正在玩" +this .game+",并出了bug" ); this .flag=!this .flag; this .notifyAll(); } public synchronized void fix () { while (flag==true ) { try { this .wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("修理工修理了" +"的bug" ); this .flag=!this .flag; this .notifyAll(); } }
如果两个for循环的次数不一样的话会卡住,因为该代码中两个线程交替执行,执行过程中会进入等待状态,等待另一个线程的唤醒,如果循环次数不一样的话,说明其中一个线程会先结束,此时另一个进入等待状态的线程无法再被唤醒,就会导致进程被卡住
结果
1.8.2 线程池
问题:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大;
思路:提前创造好多个线程,放入线程池中,使用时直接获取,使用完放回线程池中。可以避免频繁的创建销毁,可以实现线程的重复利用;
好处:
提高响应速度(减少创建新线程的事件)
降低资源消耗(重复利用线程池中的线程,不需要每次都创建新线程)
便于线程管理:
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多久后会终止
JDK5.0开始提供线程池相关API:ExecutorService 和Executors
ExecutorService:线程池接口,常见子类ThreadPoolExecutor
void executor(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable;
Futuresubmit(Callable task):执行任务,有返回值,一般用来执行Callable;
void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import java.util.concurrent.Executor;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class TestPool { public static void main (String[] args) { ExecutorService service= Executors.newFixedThreadPool(10 ); service.execute(new MyThread()); service.execute(new MyThread()); service.execute(new MyThread()); service.execute(new MyThread()); service.shutdown(); } } class MyThread implements Runnable { @Override public void run () { System.out.println(Thread.currentThread().getName()); } }
进入线程池的线程名称:pool-编号-thread-编号
结果