文章目录
1.线程简介
1.1 任务
- 多任务:多个任务都在做,但实际上在同一时间只做了一件事
1.2 进程与线程
- 进程:在操作系统中运行的程序就是进程,如qq、播放器、IDE、游戏等。进程是执行程序(程序是程序员撰写的代码,是静态的)的一次执行过程,是一个动态的概念,是系统资源分配的单位
- 进程非常大
- 一个进程有多个线程(一个进程中至少有一个线程,主线程一定存在),如视频中同时听声音、看图像、看弹幕等(每一个都有一个单独的线程控制)
- 线程是独立的执行路径,互不影响
- 一个进程中的多个线程的运行由调度器安排调度,调度器与操作系统紧密相关,先后顺序不能人为干预。
- 线程是CPU调度和执行的单位
- 线程会带来额外开销
- 对同一份资源操作时,存在资源抢夺的问题,需要加入并发控制
1.3 多线程
- 多线程:多个任务在同一时间做,
1.4 普通方法调用和多线程
- 普通方法:效率低
- 多线程:
2.线程实现(重点)
2.1 线程创建
- Thread:继承Thread类(重点)
- 创建步骤:
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start方法启动线程
- 代码:
package example1; // 创建线程方式一 public class StartThread1 extends Thread { // 线程入口 @Override public void run(){ // 线程体 for (int i = 0; i < 6; i++) { System.out.println("a "+i); } } public static void main(String[] args){ // 主线程 // 创建线程对象 StartThread1 t=new StartThread1(); // 启动线程,线程不一定立即执行,由CPU安排调度 // 不能直接调用run方法 t.start(); for (int i = 0; i < 6; i++) { System.out.println("b "+i); } } }
- 两条线程同时交替执行,线程开启不一定立即执行,由cpu调度执行。每次执行的结果都不一样
- 不建议使用:无法避免oop单继承局限性
- 实例:多线程下载网络图片
注意要使用commons-io包,需要自己引入package example1; 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 webDownloader=new WebDownloader(); webDownloader.downloader(url,name); System.out.println("下载了文件名为:"+ name ); } public static void main(String[] args){ TestThread2 t1=new TestThread2("https://i1.hdslb.com/bfs/archive/55a3c63cf52355f0b35179758fc4df74c8c29b75.jpg@412w_232h_1c.jpg","1.jpg"); TestThread2 t2=new TestThread2("https://i0.hdslb.com/bfs/feed-admin/e2566bc67196ba0f6cd633146d81eec476abcee3.jpg@412w_232h_1c","2.jpg"); TestThread2 t3=new TestThread2("https://i0.hdslb.com/bfs/feed-admin/c22765645393e853d4139cf23eb79ef5f689690b.jpg@320w_184h_1c_100q.jpg","3.jpg"); t1.start(); t2.start(); t3.start(); } } // 下载器 class WebDownloader { // 下载方法 public void downloader(String url, String name) { try { // 拷贝链接的内容到文件中 FileUtils.copyURLToFile(new URL(url), new File(name)); } catch (IOException e) { e.printStackTrace(); System.out.println("IO异常,下载失败"); } } }
- 创建步骤:
- Runnable:实现Runnable接口(重点)
- 创建步骤:
- 定义MyRunnable类实现Runnable接口
- 实现run()方法,编写线程执行体
- 创建线程对象,调用start方法启动线程
- 代码:
package example1; // 创建线程方式2,实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法 public class TestThread3 implements Runnable{ @Override public void run(){ // 线程体 for (int i = 0; i < 6; i++) { System.out.println("a "+i); } } public static void main(String[] args){ // 创建Runnbale接口的实现类对象 TestThread3 testThread3=new TestThread3(); // 创建线程对象,通过线程对象来开启我们的线程,代理,将实现类丢入线程 Thread thread=new Thread(testThread3); // 开启线程 thread.start(); // 简写 // new Thread(testThread3).start(); for (int i = 0; i < 6; i++) { System.out.println("b "+i); } } }
- 建议使用:避免了单继承局限性,灵活方便,方便同一个对象被多个线程使用
package example1; // 多个线程同时操作同一个对象 // 买火车票的例子 // 多个线程操作同一个资源时,线程不安全,数据紊乱。应解决并发问题 public class TestThread4 implements Runnable { // 票数 private int ticketNumbers = 10; @Override public void run() { while (true) { if (ticketNumbers <= 0) { break; } // 模拟延时 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } // Thread.currentThread().getName()用来获得当前线程的名字 System.out.println(Thread.currentThread().getName() + ":拿到了第" + ticketNumbers-- + "张票"); } } public static void main(String[] args) { TestThread4 ticket = new TestThread4(); new Thread(ticket, "1").start(); new Thread(ticket, "2").start(); new Thread(ticket, "3").start(); } }
- 实例:多线程下载网络图片
注意要使用commons-io包,需要自己引入package example1; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.URL; // 实现多线程同步下载图片 public class TestThread2 implements Runnable { private String url; // 网络图片的地址 private String name; // 保存的文件名 public TestThread2(String url, String name) { this.url = url; this.name = name; } // 下载图片线程的执行体 @Override public void run() { WebDownloader webDownloader=new WebDownloader(); webDownloader.downloader(url,name); System.out.println("下载了文件名为:"+ name ); } public static void main(String[] args){ // 实现Runnable接口 TestThread2 t1=new TestThread2("https://i1.hdslb.com/bfs/archive/55a3c63cf52355f0b35179758fc4df74c8c29b75.jpg@412w_232h_1c.jpg","1.jpg"); TestThread2 t2=new TestThread2("https://i0.hdslb.com/bfs/feed-admin/e2566bc67196ba0f6cd633146d81eec476abcee3.jpg@412w_232h_1c","2.jpg"); TestThread2 t3=new TestThread2("https://i0.hdslb.com/bfs/feed-admin/c22765645393e853d4139cf23eb79ef5f689690b.jpg@320w_184h_1c_100q.jpg","3.jpg"); new Thread(t1).start(); new Thread(t2).start(); new Thread(t3).start(); } } // 下载器 class WebDownloader { // 下载方法 public void downloader(String url, String name) { try { // 拷贝链接的内容到文件中 FileUtils.copyURLToFile(new URL(url), new File(name)); } catch (IOException e) { e.printStackTrace(); System.out.println("IO异常,下载失败"); } } }
- 创建步骤:
- Callable(了解):实现Callable接口
- 创建步骤
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务
ExecutorService ser=Executors.newFixedThreadPool(1);
- 提交执行
Future<Boolean> result1=ser.submit(1);
- 获取结果
boolean r1=result1.get();
- 关闭服务
ser.shutdownNow();
- 代码实例:
package example1; import java.util.concurrent.*; // 线程创建方式3,实现callable接口 public class TestCallable implements Callable<Boolean> { private String url; // 网络图片的地址 private String name; // 保存的文件名 public TestCallable(String url, String name) { this.url = url; this.name = name; } // 下载图片线程的执行体 @Override public Boolean call() throws Exception { WebDownloader webDownloader=new WebDownloader(); webDownloader.downloader(url,name); System.out.println("下载了文件名为:"+ name ); return true; } public static void main(String[] args){ // 创建目标对象 TestCallable t1=new TestCallable("https://i1.hdslb.com/bfs/archive/55a3c63cf52355f0b35179758fc4df74c8c29b75.jpg@412w_232h_1c.jpg","1.jpg"); TestCallable t2=new TestCallable("https://i0.hdslb.com/bfs/feed-admin/e2566bc67196ba0f6cd633146d81eec476abcee3.jpg@412w_232h_1c","2.jpg"); TestCallable t3=new TestCallable("https://i0.hdslb.com/bfs/feed-admin/c22765645393e853d4139cf23eb79ef5f689690b.jpg@320w_184h_1c_100q.jpg","3.jpg"); // 创建执行服务 ExecutorService ser= Executors.newFixedThreadPool(3); //提交执行,类似于start Future<Boolean> result1=ser.submit(t1); Future<Boolean> result2=ser.submit(t2); Future<Boolean> result3=ser.submit(t3); // 获取结果 try { boolean r1=result1.get(); boolean r2=result2.get(); boolean r3=result3.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }; ser.shutdownNow(); } }
- 创建步骤
2.2 案例:龟兔赛跑-Race
package example1;
// 模拟龟兔赛跑
public class Race implements Runnable {
// 胜利者
private static String winner;
@Override
public void run() {
// 假定赛道长为100米
for (int i = 0; i <= 100; i++) {
// 模拟兔子休息
if(Thread.currentThread().getName().equals("兔子") && i%10==0){
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 is "+winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
// 赛道只有一条,两个人一起跑
Race race=new Race();
new Thread(race,"兔子").start();
new Thread(race,"乌龟").start();
}
}
3.静态代理
- 静态代理模式:
- 真实对象和代理对象都要实现同一个接口
- 代理对象要代理真实角色
- 代理对象可以做许多真实对象做不了的事
- 真实对象可以专注做自己的事
- 例子:
package example1; import java.util.Arrays; public class StaticProxy { public static void main(String[] args) { You you=new You(); // 使用lambda表达式实现runnbale接口 new Thread(()-> System.out.println("我爱你")).start(); new WeddingCompany(new You()).marry(); } } interface Marry{ void marry(); } // 真实角色 class You implements Marry{ @Override public void marry() { System.out.println("happy"); } } // 代理角色 class WeddingCompany implements Marry{ private Marry target; public WeddingCompany(Marry target){ this.target=target; } @Override public void marry() { before(); this.target.marry(); // 真实对象 after(); } private void after() { System.out.println("结婚之后收尾款"); } private void before() { System.out.println("结婚之前布置现场"); } }
- Thread类也实现了Runnable接口,就相当于上面的WeddingCompany类,代理真实的Runnable接口(这里真实的Runnable接口就相当于上面的You类)。
4.线程状态
- new一个Thread的时候就是创建状态
- 调用start之后就进入就绪状态
- CPU调度该线程之后,线程才进入运行状态
- 调用sleep或者其他方法,线程就进入阻塞状态
- 线程运行完之后就进入死亡状态
4.1 线程方法
- setPriority(int newPriority):更改线程优先级
- static void sleep(long millis):在指定的毫秒内让当前正在执行的线程休眠
- void join():等待该线程终止
- static void yield():暂停当前正在执行的线程对象,并执行其他线程
- void interrupt():中断线程,不建议使用
- boolean isAlive():测试线程是否处于活动状态
4.1.1 停止线程
- 不推荐使用JDK提供的方法
- 可以使用一个标志位进行终止变量,当flag=false时,则终止线程运行
- eg:
package example1; /** * 1. 建议线程正常停止---> 利用次数,不建议死循环 * 2. 建议使用标志位 * 3. 不要使用destroy或stop等jdk不建议使用的方法 */ 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); // 调用stop方法切换标志位,让线程停止 if(i==900) { testStop.stop(); System.out.println("线程停止"); } } } }
4.1.2 线程休眠
- sleep指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException
- sleep时间到达后线程进入就绪状态
- sleep可以模拟网络延时、倒计时等
- 每一个对象都有一个锁,sleep不会释放锁
- Eg:模拟倒计时
package example1; // 模拟倒计时 public class TestSleep2{ public static void tenDown(){ 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(S tring[] args) { tenDown(); } }
- eg:打印系统时间
// 打印当前系统时间 Date startTime = new Date(System.currentTimeMillis()); // 获取系统当前时间 while(true){ try { Thread.sleep(1000); System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime)); startTime = new Date(System.currentTimeMillis()); // 更新时间 } catch (InterruptedException e) { e.printStackTrace(); } }
4.1.3 线程礼让 yield
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度,礼让不一定成功,取决于CPU
- 通俗的来讲就是同时有A、B两个线程。刚开始时,A进入CPU开始执行,A想要礼让B,A调用礼让方法出来,出来后仍然是A和B两个线程同时进行。此时,若CPU仍然调度执行A,则礼让失败;否则礼让成功。
- Eg:
若输出:package example1; // 测试线程礼让 // 礼让不一定成功 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() + " 线程停止执行"); } }
则礼让成功A 线程开始执行 B 线程开始执行 A 线程停止执行 B 线程停止执行
若输出:
则礼让失败A 线程开始执行 A 线程停止执行 B 线程开始执行 B 线程停止执行
4.1.4 Join
- Join合并线程,待此线程完成后,再执行其他线程,其他线程阻塞
- 类似于插队
-
刚开始main线程和vip线程交替执行,但当i=200时,调用了join方法,使得vip线程执行完之后才能执行main线程package example1; // 测试join方法 public class TesatJoin implements Runnable { @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("线程vip来了 "+i); } } public static void main(String[] args) { TesatJoin tesatJoin=new TesatJoin(); Thread thread=new Thread(tesatJoin); thread.start(); for (int i = 0; i < 500; i++) { if(i==200){ try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("main "+i); } } }
4.2 线程状态观测
package example1;
import java.util.Arrays;
// 观测线程状态
public class TestState {
public static void main(String[] args) {
// 采用lambda表达式定义线程体
Thread thread=new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("");
});
// 观察状态
Thread.State state=thread.getState();
System.out.println(state); // 还没调用start,状态为new
thread.start();
state=thread.getState();
System.out.println(state); // 线程启动,状态为Runnable
while(state!=Thread.State.TERMINATED){ // 只要线程不终止,就一直输出状态
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
state=thread.getState(); // 更新线程状态
System.out.println(state); // 输出状态
}
}
}
注意一个线程只能start一次,死亡后就无法再start
4.3 线程优先级
- 线程调度器按照优先级决定应该调度哪个线程来执行
- 用数字表示,范围从1-10
- 使用getPriority方法可以获得优先级
- 使用setPriority(int xxx)可以改变优先级
- 线程优先级高也不一定先执行,但是权重变大,更有可能先被线程调度器执行
- 优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,都取决于CPU的调度
- 性能倒置
- 例子:
package example1; import java.util.Arrays; // 测试线程次优先级 public class TestPriority { public static void main(String[] args) { // 主线程默认优先级,无法改变 System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority()); myPriority myPriority = new myPriority(); Thread t1 = new Thread(myPriority); Thread t2 = new Thread(myPriority); Thread t3 = new Thread(myPriority); Thread t4 = new Thread(myPriority); Thread t5 = new Thread(myPriority); Thread t6 = new Thread(myPriority); // 先设置优先级再启动 t1.start(); t2.setPriority(1); t2.start(); t3.setPriority(4); t3.start(); t4.setPriority(Thread.MAX_PRIORITY); // 10 t4.start(); t5.setPriority(8); t5.start(); t6.setPriority(7); t6.start(); } } class myPriority implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority()); } }
4.4 守护(daemon)线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕,如main线程
- 虚拟机不用等待守护线程执行完毕,如gc线程
- 例子:守护线程会在用户线程执行完毕后过一段时间消亡
package example1; // 测试守护线程 // 上帝守护你 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(); // 用户线程启动 } } // 上帝 class God implements Runnable { @Override public void run() { while (true) { System.out.println("上帝保佑着你"); } } } // 你 class you implements Runnable { @Override public void run() { for (int i = 0; i < 36500; i++) { System.out.println("活着"); } System.out.println("goodbye world"); } }
5.线程同步(重点)
多个线程操作同一个资源
- 并发:同一个对象被多个线程同时操作
- 多个线程访问一个对象,并且某些线程还想修改这个对象,这时就需要线程同步。线程同步就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池。等待前面线程使用完毕,下一个线程再使用
- 线程同步形成条件:队列+锁
- 同一进程的多个线程共享同一块存储空间,带来冲突问题
- 为了保证数据在方法中被访问是的正确性,在访问时加入锁机制synchronized。类似于数据库中事务的性质
- 当一个线程获得对象的排它锁,独占资源,其他线程必须等待。使用后释放锁即可。存在以下问题:
- 一个线程持有锁可能会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题
5.1 三大不安全案例
-
不安全的买票
- 代码:代码中没有让顾客排队买票,因此会出现线程不安全,存在重复得到一张票或者负数等情况
package syn; // 不安全的买票 // 线程不安全,存在重复得到一张票或者负数等情况 public class unsafeBuyTicket { public static void main(String[] args) { BuyTicket buyTicket=new BuyTicket(); new Thread(buyTicket,"1").start(); new Thread(buyTicket,"2").start(); new Thread(buyTicket,"3").start(); } } class BuyTicket implements Runnable { private int ticketNums = 10; boolean flag = true; // 外部停止标志 @Override public void run() { // 买票 while (flag) { buy(); } } // 修改:解决安全性问题 // synchronized 同步方法,实现锁机制,锁的是this // private synchronized void buy() { private void buy() { // 判断是否有票 if (ticketNums <= 0) { flag = false; return; } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 买票 System.out.println(Thread.currentThread().getName() + "拿到第" + ticketNums-- + "票"); } }
- 代码:代码中没有让顾客排队买票,因此会出现线程不安全,存在重复得到一张票或者负数等情况
-
不安全的取钱
- 代码:sleep放大了问题的发生性,导致最后账户可能出现余额为-50万,即两人取钱的时候都看到账户有1 00万余额,都成功的取了出来。但实际上一共取了150万,不安全的取钱。
package syn; // 不安全的取钱 // 两个人去银行取钱 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; } @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); // Thread.currentThread().getName() == this.getName() System.out.println(this.getName() + " 手里的钱:" + nowMoney); } }
- 这里同步锁要锁的对象是账户,而synchronized方法默认锁的是this,因此在run前面加上synchronized锁的就是银行,达不到目的。因此需要synchronized块来锁定account对象。锁的对象就是需要增删改的对象。
更改的代码://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); // Thread.currentThread().getName() == this.getName() System.out.println(this.getName() + " 手里的钱:" + nowMoney); } }
- 代码:sleep放大了问题的发生性,导致最后账户可能出现余额为-50万,即两人取钱的时候都看到账户有1 00万余额,都成功的取了出来。但实际上一共取了150万,不安全的取钱。
-
线程不安全的集合
- 代码:线程不安全。多个线程在同一瞬间操作了同一个位置,把多个元素添加到了同一个位置,覆盖了之前的操作,因此最后list的大小没有达到10000。
package syn; import java.util.ArrayList; import java.util.Arrays; import java.util.List; // 线程不安全的集合 public class unsafeList { public static void main(String[] args) { List<String> list=new ArrayList<>(); for (int i = 0; i < 10000; i++) { // lambda 表达式 new Thread(()->{ list.add(Thread.currentThread().getName()); }).start(); } System.out.println(list.size()); } }
- 修改后的代码:这里对list进行增删改操作,因此list就是锁的对象,要在修改list的地方加上同步块
// 线程不安全的集合 public class unsafeList { public static void main(String[] args) { List<String> list=new ArrayList<>(); for (int i = 0; i < 10000; i++) { // lambda 表达式 new Thread(()->{ synchronized (list) { list.add(Thread.currentThread().getName()); } }).start(); } System.out.println(list.size()); } }
- 代码:线程不安全。多个线程在同一瞬间操作了同一个位置,把多个元素添加到了同一个位置,覆盖了之前的操作,因此最后list的大小没有达到10000。
5.2 同步方法
- synchronized关键字,包括两种用法,synchronized方法和synchronized块
- synchronized方法默认锁的是this
-
public synchronized void methos(int args){}
- synchronized块
- 同步块:synchronized(Obj){ }
- Obj称之为同步监视器
- Obj可以是任何对象,推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
- 同步监视器的执行过程如下:
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
- synchronized方法控制对象的访问,每个对象对应一把锁,每个synchronized方法必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
- 若将一个大的方法申明为synchronized将会影响效率
- 方法里面需要修改的内容才需要锁。锁的太多会浪费资源
- 锁的对象就是需要增删改的对象。
5.3 JUC
- JUC是并发包,java.until.current
- 使用CopyOnWriteArrayList就不需要再使用同步块,与普通的ArrayList不同,CopyOnWriteArrayList已经考虑了同步的问题
package syn; // java.util.concurrent是并发包 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(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); } }
5.4 死锁
- 多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源、都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能发生死锁。
- 简单来说,就是多个线程互相抱着对方需要的资源,然后形成僵持
- 例子:
这个例子中会导致死锁,即程序先输出灰姑娘获得口红的锁,然后再输出白雪公主获得镜子的锁,随后程序就会卡住。因为接下去灰姑娘想要获得镜子的锁,而该锁已经被白雪公主占有。白雪公主想要获得口红的锁,而该锁已经被灰姑娘占有。形成死锁。package example1; // 多个线程互相抱着对方需要的资源,然后形成僵持 public class DeadLock { public static void main(String[] args) { Makeup m1=new Makeup(0,"灰姑娘"); Makeup m2=new Makeup(1, "白雪公主"); m1.start(); m2.start(); } } // 口红 class Lipstick { } // 镜子 class Mirror { } // 化妆 class Makeup extends Thread { // 需要的资源只有一份,用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() { makeUp(); } // 化妆,互相持有对方的锁,就是需要拿到对方的资源 private void makeUp(){ if(choice==0){ synchronized (lipstick){ // 获得口红的锁 System.out.println(this.name+"获得口红的锁"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (mirror){ // 一秒钟后获得镜子的锁 System.out.println(this.name+"获得镜子的锁"); } } }else{ synchronized (mirror){ // 获得镜子的锁 System.out.println(this.name+"获得镜子的锁"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lipstick){ // 一秒钟后获得口红的锁 System.out.println(this.name+"获得口红的锁"); } } } } }
修改makeUp方法:
这时程序就能正常执行,不会产生死锁private void makeUp(){ if(choice==0){ synchronized (lipstick){ // 获得口红的锁 System.out.println(this.name+"获得口红的锁"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (mirror){ // 一秒钟后获得镜子的锁 System.out.println(this.name+"获得镜子的锁"); } }else{ synchronized (mirror){ // 获得镜子的锁 System.out.println(this.name+"获得镜子的锁"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (lipstick){ // 一秒钟后获得口红的锁 System.out.println(this.name+"获得口红的锁"); } } }
- 产生死锁的四个必要条件:(任意一个条件不满足就可以避免死锁发生)
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
5.5 锁(Lock)
- 通过显示定义同步锁对象来实现同步。 同步锁使用Lock对象充当
- Lock是显式的,需要手动开启和关闭锁。而synchronized是隐式锁,出了作用域会自动释放。Lock锁更加清晰明了。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- Lock锁性能更好
- ReentrantLock(可重入锁)实现了Lock,拥有与synchronized相同的并发性和内存寓意,可以显式加锁、释放锁
- 仍然以买票为例:
reentrantLock.lock(); // 加锁// 测试Lock锁 public class TestLock { public static void main(String[] args) { TestLock2 testLock2=new TestLock2(); new Thread(testLock2,"1").start(); new Thread(testLock2,"2").start(); new Thread(testLock2,"3").start(); } } class TestLock2 implements Runnable{ private int ticketNums=10; // 定义lock锁 private final ReentrantLock reentrantLock=new ReentrantLock(); @Override public void run() { while(true){ try { reentrantLock.lock(); // 加锁 if(ticketNums>0){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(ticketNums--); }else { break; } }finally { // 解锁 reentrantLock.unlock(); } } } }
reentrantLock.unlock();// 解锁 - 优先使用顺序:
- Lock>同步代码块>同步方法
6.线程通信问题/线程协作
生产者消费者模式,不是设计模式之一,是一个问题
6.1 应用场景:生产者和消费者问题
- 假设仓库中只能存放一件产品,生产者生产产品并放入仓库,消费者取走产品消费
- 若仓库中没有产品,则生产者将产品放入仓库。否则停止生产并等待,直到仓库中的产品被消费者取走为止
- 若仓库中有产品,则消费者取走产品。否则停止消费并等待,直到仓库再次放入产品为止
- 是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
- 仅有synchronized是不够的,synchronized不能实现不同线程之间消息传递
- java提供解决线程通信问题的方法:
上述方法只能在同步方法或者同步代码块中使用,否则会抛出异常方法 作用 wait() 线程一直等待直到其他线程通知,与sleep不同,会释放锁 wait(long timeout) 指定等待的毫秒数 notify() 唤醒一个处于等待状态的线程 notifyAll() 唤醒一个对象上所有调用wait()方法的线程,优先级高的线程优先调度 - 解决问题的方法
- 管程法:生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
通过wait方法进行等待,通过notifyAll方法进行通知public class TestPC { public static void main(String[] args) { SyncBuffer syncBuffer=new SyncBuffer(); new Productor(syncBuffer).start(); new Customer(syncBuffer).start(); } } // 生产者 class Productor extends Thread { SyncBuffer syncBuffer; public Productor(SyncBuffer syncBuffer) { this.syncBuffer = syncBuffer; } @Override public void run() { for (int i = 0; i < 100; i++) { syncBuffer.push(new Product(i)); System.out.println("生产了第"+i+"个产品"); } } } // 消费者 class Customer extends Thread { SyncBuffer syncBuffer; public Customer(SyncBuffer syncBuffer) { this.syncBuffer = syncBuffer; } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("消费了第"+syncBuffer.pop().id+"个产品"); } } } // 产品 class Product { int id; // 产品编号 public Product(int id) { this.id = id; } } // 缓冲区 class SyncBuffer { // 缓冲区大小 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(); } } // 若没有满,就需要放入产品 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; } }
- 信号灯法:通过标志位判断是要等待还是唤醒另一个线程
package gaoji; // 生产者消费者问题 // 信号灯法 // 通过标志位 public class TestPC2 { public static void main(String[] args) { TV tv=new TV(); new Player(tv).start(); new Watcher(tv).start(); } } // 生产者-演员 class Player extends Thread { TV tv; public Player(TV tv){ this.tv=tv; } @Override public void run() { for (int i = 0; i < 20; i++) { if(i%2==0){ this.tv.play("抖音"); }else{ this.tv.play("快本"); } } } } // 消费者-观众 class Watcher extends Thread { TV tv; public Watcher(TV tv){ this.tv=tv; } @Override public void run() { for (int i = 0; i < 20; i++) { this.tv.watch(); } } } // 产品-节目 class TV { // 演员表演时,观众等待。观众观看时,演员等待 String name; //表演的节目 // flag为真时,演员表演,观众等待;flag为假时,演员等待。 boolean flag = true; // 演员表演 public synchronized void play(String voice) { // 演员等待 if(!flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("演员表演了" + voice); // 通知观众观看 this.notifyAll(); this.name = voice; this.flag = !flag; } // 观众观看 public synchronized void watch() { //如果演员还没有表演结束,即flag=true,观众等待 if (flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("观众观看了" + this.name); // 观众看完了要通知演员表演 this.notifyAll(); this.flag = !flag; } }
- 管程法:生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
7.线程池
- 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。就可以避免频繁的创建销毁,实现重复利用。
- 优点:
- 提高响应速度(减少创建新线程时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理:
- corePoolSize: 核心池的大小
- maximumPoolSize: 最大线程数
- keepAliveTime: 线程没有任务时最多保持多长时间后停止
- java中线程池相关的API:ExecutorService和Executors
- ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
void execute(Runnabke command) // 执行任务/命令,没有返回值,一般用来执行Runnable <T> Future<T> submit(Callabe<T> task) // 执行任务,有返回值,一般用来执行Callable void shutdown() // 关闭线程池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
- 例子:
// 测试线程池 public class TestPool { public static void main(String[] args) { // 创建服务、创建线程池 // newFixedThreadPool参数为创建的线程池的大小 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()); } }