Java——多线程

Typora写到一半崩了,文本直接没了可真是没把我弄得心态爆炸,第五到第八都是后面根据狂神的文档复制粘贴了,虽然代码还在相关的注释没了可真是难受!!!

一、线程简介

  1. 任务:执行的目标,具体由线程来实现

  2. 进程:程序是指令和数据的有序集合,是一个静态的概念;进程是执行程序的一次执行过程,是一个动态的概念.(进程是系统资源分配的单位)

  3. 线程:一个进程至少有一个线程.线程是CPU调度和执行的单位

    注:

    • 真正的多线程是指有多个cpu,即多核,如服务器.如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以有了看似并发的效果
    • 程序中的main函数可以理解为主线程
    • 后台始终保持了一个gc线程(垃圾回收线程)
  4. 多线程:多个线程的同时执行

二、线程创建的方式

  1. Thread class:继承Thread类(其实也是实现了Runnable接口)

    • 新建一个类并继承Thread类
    • 重写run方法,也就是线程执行体
    • 在主线程中new一个该对象,通过start方法执行
    package com.guan.test; public class ThreadTeast1 extends Thread{ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println("次线程" + i); } } public static void main(String[] args) { ThreadTeast1 threadTeast1 = new ThreadTeast1(); threadTeast1.start(); for (int i = 0; i < 1000; i++) { System.out.println("主线程" + i); } } }

    注:线程的调度是有延时的,由cpu调度执行.所以这里的主函数的循环建议设为1000(具体由电脑的特性决定),否则无法清晰地看到主线程的输出将次线程的输出包裹

    延伸:利用多线程下载图片

    导入相关的依赖:

    <!-- https://mvnrepository.com/artifact/commons-io/commons-io --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency>

    编写主程序:

    package com.guan.test; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; public class ThreadTest2 extends Thread{ private String url; private String name; public ThreadTest2(){ } public ThreadTest2(String url,String name){ this.url = url; this.name = name; } @Override public void run() { PictureDownloader downloader = new PictureDownloader(); try { downloader.pictureDownloader(this.url,this.name); System.out.println("下载完成:" + name); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { ThreadTest2 threadTest1 = new ThreadTest2("xxx1.png","xxx1.png"); ThreadTest2 threadTest2 = new ThreadTest2("xxx2.jpg","xxx2.jpg"); ThreadTest2 threadTest3 = new ThreadTest2("xxx3.png","xxx3.png"); threadTest1.start(); threadTest2.start(); threadTest3.start(); } } class PictureDownloader{ public void pictureDownloader(String url,String name) throws IOException { //调用了外部的图片下载工具 FileUtils.copyURLToFile(new URL(url),new File(name)); } }

    注:这里实现的一个有趣的细节,外部的数据需要作为Thread的实现类的属性放进去!

    图片大小:1>3>2

    现在完成速度:3>2>1

  2. Runnable接口:实现Runnable接口

    静态代理

    • 定义类实现Runnable接口

    • 实现run()方法,编写线程执行体

    • 创建Thread对象,同时将Runnable实现类的对象丢进去,调用start()方法启动线程

      package com.guan.test; public class ThreadTest3 implements Runnable{ public void run() { for (int i = 0; i < 20; i++) { System.out.println("次线程" + i); } } public static void main(String[] args) { ThreadTest3 threadTest3 = new ThreadTest3(); new Thread(threadTest3).start(); for (int i = 0; i < 1000; i++) { System.out.println("主线程" + i); } } }

    将上一个图片下载的类改成用Runnable实现

    package com.guan.test; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; public class ThreadTest2 implements Runnable{ private String url; private String name; public ThreadTest2(){ } public ThreadTest2(String url,String name){ this.url = url; this.name = name; } public void run() { PictureDownloader downloader = new PictureDownloader(); try { downloader.pictureDownloader(this.url,this.name); System.out.println("下载完成:" + name); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { ThreadTest2 threadTest1 = new ThreadTest2("http://icwl.org.cn/images/sponsors1.png","sponsors1.png"); ThreadTest2 threadTest2 = new ThreadTest2("http://icwl.org.cn/images/sponsors2.jpg","sponsors2.jpg"); ThreadTest2 threadTest3 = new ThreadTest2("http://icwl.org.cn/images/sponsors3.png","sponsors3.png"); //主要是这里用了静态代理方式的实现 new Thread(threadTest1).start(); new Thread(threadTest2).start(); new Thread(threadTest3).start(); } } class PictureDownloader{ public void pictureDownloader(String url,String name) throws IOException { FileUtils.copyURLToFile(new URL(url),new File(name)); } }

    静态代理的实现:(从三个对象说起)

    • 抽象角色:真实角色实现的业务,通常为接口,且被真实角色和代理角色实现(其实代理角色实现的接口就是与之相对应的真实角色的接口,当然可能还增加了一些业务).Runable接口,主要是run方法的实现(Thread类中的Runnable类型的字段——target)
    • 真实角色:ThreadTest3类,也就是被代理的对象(Thread中target属性的实例)
    • 代理角色:Thread类,代理了ThreadTest3要实现的操作,同时添加了一些附属的操作,比如开启了一个新的线程进行代理

    优点:避免了单继承的局限性,灵活方便,同一个对象,可以被多个线程使用

    缺点:在使用同一个对象的情况下,可能多个线程操作同一个资源造成线程不安全

    package com.guan.test; public class ThreadTest4 implements Runnable { private int resources = 10; public void run() { while(true){ //获得当前线程的名字 System.out.println(Thread.currentThread().getName() + "抢到了第" + resources-- + "张票"); try { //小明的线程第一个开启,为了防止他一下子抢完票,每次先睡0.2s Thread.currentThread().sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } if (resources <=1){ break; } } } public static void main(String[] args) { ThreadTest4 test4 = new ThreadTest4(); new Thread(test4,"小明").start(); new Thread(test4,"老师").start(); new Thread(test4,"黄牛党").start(); } }

    注:几个人之间很可能抢到相同的一张票

  3. Callable接口:实现Callable接口

    • 实现Callable接口,需要返回值类型(接口的返回值????)
    • 重写call方法,需要抛出异常
    • 创建目标对象
    • 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
    • 提交执行:Future<Boolean> result1 = ser.submit(t1);
    • 获取结果:boolean r1 = result1.get();
    • 关闭服务:ser.shutdownNow();

    优点:

    • 可以定义返回值
    • 可以抛出异常

    初次使用:(后面还会详细讲到Executor类)

    package com.guan.test; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.concurrent.*; public class ThreadTest6 implements Callable<Boolean> { private String url; private String name; public ThreadTest6(){ } public ThreadTest6(String url,String name){ this.url = url; this.name = name; } //该方法的返回值为布尔型 public Boolean call() { PictureDownloader downloader = new PictureDownloader(); try { downloader.pictureDownloader(this.url,this.name); System.out.println("下载完成:" + name); } catch (IOException e) { e.printStackTrace(); return false; } return true; } public static void main(String[] args) throws ExecutionException, InterruptedException { ThreadTest6 threadTest1 = new ThreadTest6("http://icwl.org.cn/images/sponsors1.png","sponsors1.png"); ThreadTest6 threadTest2 = new ThreadTest6("http://icwl.org.cn/images/sponsors2.jpg","sponsors2.jpg"); ThreadTest6 threadTest3 = new ThreadTest6("http://icwl.org.cn/images/sponsors3.png","sponsors3.png"); //建立连接池 ExecutorService ser = Executors.newFixedThreadPool(3); //可以创建的线程数量为3 //提交线程 Future<Boolean> result1 = ser.submit(threadTest1); Future<Boolean> result2 = ser.submit(threadTest2); Future<Boolean> result3 = ser.submit(threadTest3); boolean r1 = result1.get(); boolean r2 = result2.get(); boolean r3 = result3.get(); System.out.println(r1); System.out.println(r2); System.out.println(r3); //关闭连接池 ser.shutdownNow(); } }

    这里看一下Callable接口中的call方法

    @FunctionalInterface public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; }

    注:v是call的返回值.当我们实现这一接口时,需要将v的类型写在接口的附近(如上例中的返回类型就是Boolean):

    public class ThreadTest6 implements Callable<Boolean>

三、Lambda表达式(函数式编程)

使用前提:函数式接口

函数式接口的定义:

  1. 任何接口,如果只包含唯一一个抽象方法,那它就是一个函数式接口
  2. 对于函数式接口,我们可以通过lambda表达式来创建该接口的对象

优化过程:内部实现类->静态内部类->局部内部类->匿名内部类(没有类的名称,必须借助**接口**或者父类)->lambda表达式简化(可以简化参数类型,括号,花括号)

代码:

package com.guan.test; public class ThreadTest7 { //静态内部类 static class ILove2 implements Love{ public void ILove() { System.out.println("I love 2"); } } public static void main(String[] args) { //局部内部类 class ILove3 implements Love{ public void ILove() { System.out.println("I love 3"); } } ILove1 iLove1 = new ILove1(); ILove2 iLove2 = new ILove2(); ILove3 iLove3 = new ILove3(); //匿名内部类 Love iLove4 = new Love(){ public void ILove() { System.out.println("I love 4"); } }; //这是个语句,所以需要分号作为结尾 //箭头函数 Love iLove5 = ()->{ System.out.println("I love 5"); }; iLove1.ILove(); iLove2.ILove(); iLove3.ILove(); iLove4.ILove(); iLove5.ILove(); } } //函数式接口 interface Love{ public abstract void ILove(); } //内部实现类 class ILove1 implements Love{ public void ILove() { System.out.println("I love 1"); } }

优点:

  1. 避免匿名内部类定义过多
  2. 让代码看上去更加简洁
  3. 去掉一堆没有意义的代码,只留下核心的逻辑

应用场景:多线程中的Runnable接口

注:在idea中的使用需要进行一些设置,详细可见这篇文章https://blog.csdn.net/mtngt11/article/details/100052996

四、线程状态

状态:创建,就绪,运行,阻塞,死亡

相关方法:

  • setPriority:更改线程的优先级
  • sleep:让当前正在执行的线程休眠
  • join:等到该线程终止
  • yield:暂停当前正在执行的线程对象,并执行其它线程
  • interrupt:中断线程
  • isAlive:测试线程是否处于活动状态

停止线程:

  • 建议线程正常停止:利用次数,不推荐死循环

  • 建议使用一个标志位(flag),比如保持线程是在flag=true的情况下才能运行

  • 不要用过时或者JDK不建议使用的方法

    package com.guan.test; public class ThreadTest8 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) { ThreadTest8 thread = new ThreadTest8(); new Thread(thread).start(); for (int i = 0; i < 1000; i++) { System.out.println("主线程正在跑" + i); if (i == 900){ thread.stop(); System.out.println("次线程停下了!"); } } } }
  1. 线程休眠

    特点:

    • sleep(时间)指定当前线程阻塞的毫秒数
    • sleep存在异常InterruptedException
    • sleep时间达到后线程进入就绪状态
    • sleep可以模拟网络延时,倒计时等
    • 每一个对象都有一个锁,sleep不会释放锁

    作用:

    • 模拟网络延时,放大问题的发生性

      package com.guan.test; public class ThreadTest4 implements Runnable { private int resources = 10; public void run() { while(true){ System.out.println(Thread.currentThread().getName() + "抢到了第" + resources-- + "张票"); try { //小明的线程第一个开启,为了防止他一下子抢完票,每次先睡0.2s Thread.currentThread().sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } if (resources <=1){ break; } } } public static void main(String[] args) { ThreadTest4 test4 = new ThreadTest4(); new Thread(test4,"小明").start(); new Thread(test4,"老师").start(); new Thread(test4,"黄牛党").start(); } }

      注:还是这个案例,如果不睡一下,所有票都会被小明抢到,因为小明这个线程是最先开启的,且抢票太快了.如果睡一下,又会发现线程是不安全的,因为有多个人很可能正好抢到了同一张票

    • 模拟倒计时,打印当前时间(如果采用的不是主线程可能无法拿到控制权,因此这个方案是不太严谨的)

      package com.guan.test; import java.text.SimpleDateFormat; import java.util.Date; public class ThreadTest9 { public static void TenDown() throws InterruptedException { int now = 10; while(now>0){ System.out.println(now--); //这里相当于睡的是主线程 Thread.sleep(1000); } } public static void logTime() throws InterruptedException { while(true){ Date date = new Date(System.currentTimeMillis()); System.out.println(new SimpleDateFormat("HH:mm:ss").format(date)); Thread.sleep(1000); } } public static void main(String[] args) throws InterruptedException { ThreadTest9.TenDown(); } }
  2. 线程休眠

    特点:

    • 礼让线程,让当前正在执行的线程暂停,但不阻塞

    • 将线程从运行状态转为就绪状态

    • 让cpu重新调度,礼让不一定成功,和具体的CPU调度算法有关

      一个不太严谨的测试:

      package com.guan.test; public class ThreadTest10 implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + "线程开始执行"); Thread.yield(); System.out.println(Thread.currentThread().getName() + "线程结束执行"); } public static void main(String[] args) { ThreadTest10 test10 = new ThreadTest10(); new Thread(test10,"a").start(); new Thread(test10,"b").start(); } }
  3. 合并进程

    特点:

    • Join合并继承,待此进程执行完成后,再执行其他线程,其他线程阻塞

      package com.guan.test; public class ThreadTest11 implements Runnable { @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("次线程在跑:" + i); } } public static void main(String[] args) throws InterruptedException { ThreadTest11 test11 = new ThreadTest11(); Thread thread = new Thread(test11); thread.start(); for (int i = 0; i < 1000; i++) { System.out.println("主线程在跑:" + i); if(i==500){ //在副线程跑的过程中,让位给主线程跑 thread.join(); } } } }
  4. 观测线程的状态

    package com.guan.test; public class ThreadTest12 implements Runnable{ @Override public void run() { System.out.println("线程开始了"); for (int i = 0; i < 5; i++) { try { // System.out.println("睡"); Thread.currentThread().sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("线程结束"); } public static void main(String[] args) throws InterruptedException { ThreadTest12 test12 = new ThreadTest12(); Thread thread = new Thread(test12); Thread.State state = thread.getState(); System.out.println(state); //new thread.start(); state = thread.getState(); //runnable while (state != Thread.State.TERMINATED){ state = thread.getState(); //time_waiting System.out.println(state); Thread.sleep(100); } System.out.println(state); //terminated thread.start(); } }

    注:

    • 死亡的线程无法再次启动
    • 下面输出的间隔需要定位100,如果定位1000很可能两边间隔相重叠导致输出Runable
  5. 线程优先级

    • 特点:Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有进程,线程调度器按照优先级决定应该调度哪个线程来执行

    • 优先级范围:1~10

    • 注意线程的实际执行还是要看cpu,但是优先级搞得线程执行的可能性更大

      package com.guan.test; public class ThreadTest13 extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority()); } public static void main(String[] args) { System.out.println("main --> " + Thread.currentThread().getPriority()); ThreadTest13 test1 = new ThreadTest13(); ThreadTest13 test2 = new ThreadTest13(); ThreadTest13 test3 = new ThreadTest13(); ThreadTest13 test4 = new ThreadTest13(); ThreadTest13 test5 = new ThreadTest13(); ThreadTest13 test6 = new ThreadTest13(); test1.setPriority(-1); test2.setPriority(2); test3.setPriority(4); test4.setPriority(6); test5.setPriority(8); test6.setPriority(11); test1.start(); test2.start(); test3.start(); test4.start(); test5.start(); test6.start(); } }

      注:先设置优先级再启动

  6. 守护线程(daemon)

    特点:

    • 线程分为用户线程和守护线程
    • 虚拟机必须确保用户线程执行完毕
    • 虚拟机不用等待守护线程执行完毕(可以认为,守护线程随着用户线程的消亡而消亡)
    • 如:后台记录操作日志,监控内存,垃圾回收等

    代码:

    package com.guan.test; public class ThreadTest14 { public static void main(String[] args) { God god = new God(); People people = new People(); Thread thread = new Thread(god); thread.setDaemon(true); //设置为守护线程 thread.start(); new Thread(people).start(); } } class God implements Runnable{ @Override public void run() { while(true){ System.out.println("God bless you!"); } } } class People implements Runnable{ @Override public void run() { for (int i = 0; i < 36500; i++) { System.out.println("活着的第" + i + "天"); } } }

    注:可以看到虽然守护线程永远为true,但是在用户结束后它也会结束.简单来说就是把你送走了它才走

五、线程同步机制

并发:同一个对象被多个线程同时操作

线程同步:线程同步多用于处理多线程问题(通常为多个线程访问同一个对象).线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用

每个线程在自己的工作内存交互,内存控制不当会造成数据不一致(第一个线程的工作内存有3张票,第二个线程的工作内存有5张票,而实际上可能已经没有票了,这种情况下的售票显然会得到负值)

两个测试案例:

package com.guan.test; public class ThreadTest15 implements Runnable{ private int num = 10; private Boolean flag =true; @Override public void run() { while(flag){ buy(); } } private void buy() { if (num>0){ //在读与写之间增加延时可以放大效果 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "拿到了第" + num-- + "张票"); }else{ flag = false; } } public static void main(String[] args) { ThreadTest15 test = new ThreadTest15(); new Thread(test,"1").start(); new Thread(test,"2").start(); new Thread(test,"3").start(); } }
package com.guan.test; public class ThreadTest16 implements Runnable{ Acount acount; int now; int get; public ThreadTest16(Acount acount, int now, int get) { this.acount = acount; this.now = now; this.get = get; } @Override public void run() { if(acount.num-get>=0){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } acount.num -= get; now += get; System.out.println(Thread.currentThread().getName() + "现在有:" + now + ";acount现在还有" + acount.num); }else{ System.out.println(Thread.currentThread().getName() + "取不出:" + get); } } public static void main(String[] args) { Acount acount = new Acount("基金", 100); ThreadTest16 threadTest1 = new ThreadTest16(acount,100,50); ThreadTest16 threadTest2 = new ThreadTest16(acount,100,100); new Thread(threadTest1,"小明").start(); new Thread(threadTest2,"小红").start(); } } class Acount{ String name; int num; public Acount(String name, int num) { this.name = name; this.num = num; } } /* 小明现在有:150;acount现在还有50 小红现在有:200;acount现在还有-50 原因:小明差->sleep->小红查->sleep->小明扣->小红扣 */ /* 小红现在有:200;acount现在还有0 小明现在有:150;acount现在还有0 看上去很离谱的答案:小红查且取到100数据->小明查且取到100数据->小明付款且返回50->小红付款且返还0(覆盖50)->两个人都打印出了0(50同样的道理,虽然还是很离谱,但是仔细想想所有的程序都是要编译成小段的机器码执行也就稍微可以理解了) */

注:通过这里0/0,50/50的结果我们可以极大地感受到线程的不安全性

线程不安全的集合:

  • ArrayList (两个线程再同一时间覆盖了同一位置,倒置出现数据的缺失)

    package com.guan.test; import java.util.ArrayList; public class ThreadTest17 { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(); for (int i = 0; i < 1000; i++) { new Thread(()->{ list.add(3); }).start(); } System.out.println(list.size()); } }

    注:这里实际的list.size不会有1000,因为在线程运作的过程中很可能出现两个线程共同只能用同一个位置,从而导致结果错误

解决方案:同步方法——synchronized修饰符

本质:队列+锁

优点:安全

缺点:性能以及性能倒置问题(优先级高的线程等待优先级低的线程拿到的锁)

使用:

  1. 同步方法:修饰方法,但锁的是方法的this对象

    package com.guan.test; public class ThreadTest15 implements Runnable{ private int num = 10; private Boolean flag =true; @Override public void run() { while(flag){ buy(); } } private synchronized void buy() { if (num>0){ //在读与写之间增加延时可以放大效果 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "拿到了第" + num-- + "张票"); }else{ flag = false; } } public static void main(String[] args) { ThreadTest15 test = new ThreadTest15(); new Thread(test,"1").start(); new Thread(test,"2").start(); new Thread(test,"3").start(); } }
  2. 同步块:修饰语句块,锁的对象就是变化的量,需要增删改的对象

    格式:synchronized(obj){}

    注:obj是同步监视器,通常是共享资源,如果synchronized修饰的是方法,可以认为obj就是this

    package com.guan.test; public class ThreadTest16 implements Runnable{ Acount acount; int now; int get; public ThreadTest16(Acount acount, int now, int get) { this.acount = acount; this.now = now; this.get = get; } @Override public void run() { synchronized (acount){ if(acount.num-get>=0){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } acount.num -= get; now += get; System.out.println(Thread.currentThread().getName() + "现在有:" + now + ";acount现在还有" + acount.num); }else{ System.out.println(Thread.currentThread().getName() + "取不出:" + get); } } } public static void main(String[] args) { Acount acount = new Acount("基金", 100); ThreadTest16 threadTest1 = new ThreadTest16(acount,100,50); ThreadTest16 threadTest2 = new ThreadTest16(acount,100,100); new Thread(threadTest1,"小明").start(); new Thread(threadTest2,"小红").start(); } } class Acount{ String name; int num; public Acount(String name, int num) { this.name = name; this.num = num; } }

六、死锁

定义:多个线程各自占有一些共享资源 , 并且互相等待其他线程占有的资源才能运行 , 而 导致两个或者多个线程都在等待对方释放资源 , 都停止执行的情形 . 某一个同步块 同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 “ 死锁 ” 的问题.比如两个小朋友a和b,a想要水枪,b想要小车,两个谁又不让谁,矛盾无法调和就出现了死锁

产生条件:

  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件 : 进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件 : 若干进程之间形成一种头尾相接的循环等待资源关系。
package com.guan.test; public class ThreadTest18{ public static void main(String[] args) { MakeUp hong = new MakeUp(1, "小红"); MakeUp lan = new MakeUp(2, "小兰"); new Thread(hong).start(); new Thread(lan).start(); } } class LipStick{ } class Mirro{ } class MakeUp implements Runnable{ private static LipStick lipStick = new LipStick(); private static Mirro mirro = new Mirro(); private int choice; private String name; public MakeUp(int choice, String name) { this.choice = choice; this.name = name; } public void make() { if(choice == 1){ //有口红需要镜子 synchronized (lipStick){ System.out.println(name + "有了口红"); try { Thread.currentThread().sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (mirro){ System.out.println(name + "有了镜子"); } } }else{ //有镜子需要口红 synchronized (mirro){ System.out.println(name + "有了镜子"); try { Thread.currentThread().sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lipStick){ System.out.println(name + "有了口红"); } } } } @Override public void run() { make(); } }

注:嵌套的同步块很容易造成死锁

解决方法:将同步块解开

package com.guan.test; public class ThreadTest18{ public static void main(String[] args) { MakeUp hong = new MakeUp(1, "小红"); MakeUp lan = new MakeUp(2, "小兰"); new Thread(hong).start(); new Thread(lan).start(); } } class LipStick{ } class Mirro{ } class MakeUp implements Runnable{ private static LipStick lipStick = new LipStick(); private static Mirro mirro = new Mirro(); private int choice; private String name; public MakeUp(int choice, String name) { this.choice = choice; this.name = name; } public void make() { if(choice == 1){ //有口红需要镜子 synchronized (lipStick){ System.out.println(name + "有了口红"); try { Thread.currentThread().sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (mirro){ System.out.println(name + "有了镜子"); } }else{ //有镜子需要口红 synchronized (mirro){ System.out.println(name + "有了镜子"); try { Thread.currentThread().sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (lipStick){ System.out.println(name + "有了口红"); } } } @Override public void run() { make(); } }

七、Lock(锁)

特点:

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开 始访问共享资源之前应先获得Lock对象
  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

synchronized与lock比较

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序: Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方 法体之外)
package com.guan.test; import java.util.concurrent.locks.ReentrantLock; public class ThreadTest19 implements Runnable{ private int num = 10; private final ReentrantLock reentrantLock = new ReentrantLock(); @Override public void run() { while (true){ try{ reentrantLock.lock(); //加锁 if(num>0){ try { Thread.currentThread().sleep(1000); //注意:可以获得当前线程并进行睡眠 // Thread.currentThread().sle 这个情况居然搜不到sleep方法就很离谱??? } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "获得了第" + num-- + "张票"); }else{ break; } }finally { reentrantLock.unlock(); //解锁 } } } public static void main(String[] args) { ThreadTest19 test19 = new ThreadTest19(); new Thread(test19,"小明").start(); new Thread(test19,"小红").start(); new Thread(test19,"小亮").start(); } }

注:

  • 建议将reentrantLock.lock放在try-catch外
  • 这里使用的是ReentrantLock

八、线程通信

生产者与消费者问题:这是一个线程同步问题 , 生产者和消费者共享同一个资源 , 并且生产者和消费者之 间相互依赖 , 互为条件

相关接口:

  • wait():表示线程一直等待,知道其他线程通知,与sleep不同,释放锁
  • wait(long timeout):指定等待的毫秒数
  • nitify():唤醒一个处于等待状态的线程
  • notifyAll():唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

注意:

  • 以上均是Object类的方法
  • 都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException

解决方法:

  1. 管程法:消费者不能直接使用生产者的数据 , 他们之间有个 “ 缓冲区 生产者将生产好的数据放入缓冲区 , 消费者从缓冲区拿出数据

    package com.guan.test; public class ThreadTest20 { public static void main(String[] args) { Container container = new Container(); Procedure procedure = new Procedure(container); Consumer consumer = new Consumer(container); new Thread(procedure).start(); new Thread(consumer).start(); } } class Procedure implements Runnable{ Container container; public Procedure(Container container) { this.container = container; } @Override public void run() { while (true){ container.push(new Chicken()); } } } class Consumer implements Runnable{ Container container; public Consumer(Container container) { this.container = container; } @Override public void run() { while (true){ container.pop(); } } } class Chicken{ } //容器:中间商 class Container{ Chicken[] chickens = new Chicken[11]; int count = 0; //记录chickens的数量 //添加同步锁,因为操作的都是chickens和count //生产者生成 public synchronized void push(Chicken chicken){ if(count == 10){ //生产满了,休息一下 try { System.out.println("生产满了"); this.wait(); //停止生产 } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.currentThread().sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } chickens[++count] = chicken; System.out.println("生产了第" + count + "件产品"); this.notifyAll(); //有产品出来了,通知消费 } //消费者消费 public synchronized Chicken pop(){ if(count <= 0){ try { System.out.println("没有可消费的产品"); this.wait(); //停止消费 } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费了第" + count + "件产品"); Chicken chicken = chickens[count--]; //消费产品 this.notifyAll(); return chicken; } }

    注:线程通信的方法都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException

  2. 信号灯法:使用标志位进行通信

    package com.guan.test; //信号灯法解决生产者与消费者问题 public class ThreadTest21 { public static void main(String[] args) { Road road = new Road(); Human human = new Human(road); Car car = new Car(road); new Thread(human).start(); new Thread(car).start(); } } class Road{ //true表示行人过,false表示车过 Boolean flag = true; public synchronized void humanGo(){ //如果现在是车过,行人就等着 if(!flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = !flag; System.out.println("人过了"); //通知车可以走了 this.notify(); } public synchronized void carGo(){ if(flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = !flag; System.out.println("车走了"); this.notify(); } } class Human implements Runnable{ private Road road; public Human(Road road) { this.road = road; } @Override public void run() { while (true){ road.humanGo(); } } } class Car implements Runnable{ private Road road; public Car(Road road) { this.road = road; } @Override public void run() { while (true){ road.carGo(); } } }

九、线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大.

解决思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完再放回去,可以避免频繁创建销毁,实现重复利用.类似生活中的公共交通

好处:

  • 提高响应速度
  • 降低资源消耗
  • 便于线程管理(核心池的大小,最大线程数,线程没有任务时最多保持多长时间后会终止)

相关类:

  1. ExecutorService:真正的线程池接口.常见的子类:ThreadPoolExecutor
    • void execute()Runnable command:执行任务/命令,无返回值,一般用来执行Runnable
    • Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
    • void shutdown():关闭连接池
  2. Executors:工具类、线程池的工厂类,用于创建并返回不同的类型的线程池
package com.guan.test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadTest22 implements Runnable{ int i=0; public static void main(String[] args) { //开启线程池 ExecutorService service = Executors.newFixedThreadPool(10); //线程池的大小 //线程池执行 ThreadTest22 test = new ThreadTest22(); service.execute(test); service.execute(test); service.execute(test); service.execute(test); service.execute(test); //关闭线程池 service.shutdown(); } @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行了第:" + ++i + "次"); } }

__EOF__

本文作者Arno
本文链接https://www.cnblogs.com/Arno-vc/p/13637544.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   Arno_vc  阅读(96)  评论(1编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示