admin 管理员组

文章数量: 1087649

狂神说多线程笔记

1.基本概念

Process和Thread

程序:指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

进程:是执行程序的一次执行过程,是一个动态的概念。是系统资源分配的单位。

​ 一个进程可以包含有多个线程(如视频中同时听到声音、看到图像,还可以看弹幕)

​ 一个进程至少有一个线程,否则无存在的意义。

线程:CPU调度和执行的单位。

注意:很多多线程是模拟出来,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,cpu只能执行一个代码,因为切换很快,所以就有同时执行的错觉。

2.核心概念

  • 线程就是独立的执行路径;

  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;

  • main()称为主线程,为系统的入口,用于执行整个程序;

  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为干预的;

  • 对同一份资源操作时,会存在资源抢夺问题,需要加入并发控制;

  • 线程会带来额外的开销,如CPU调度时间,并发控制开销;

  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

3.线程创建

1.继承Thread类,重写run方法

1.1创建Thread类

2.2重写run()方法

3.3调用start()开启线程

package com.xia.Thread;public  class ThreadTest {// 继承 Thread 类并重写 run 方法public static class MyThread extends Thread{@Overridepublic void run() {for(int i=0;i<1000;i++){System.out.println("狗尧来学Java");}}}public static void main(String[] args) {//创建一个线程MyThread thread = new MyThread();//启动线程thread.start();for(int i=0;i<1000;i++){System.out.println("杰哥是最帅的"+i);}
}

例如:

网上下载图片保存在指定目录

package com.xia.thread;import org.apache.commons.io.FileUtils;import java.io.File;
import java.io.IOException;
import java.net.URL;//练习Thread,使用多线程同步下载图片
public class ThreadTest1 extends Thread{private String url;//图片的网址。private String name;//图片的名字public ThreadTest1(String url, String name) {this.url = url;this.name = name;}//线程的执行体@Overridepublic void run() {WebDownLoader webDownLoader=new WebDownLoader();webDownLoader.downLoader(url,name,"C:\\Users\\xj\\Desktop\\照片\\狗尧来");//下载到指定的地方,默认下载到根目录(最后图片会出现在桌面的’照片‘文件夹里,名为’狗尧来1.png‘)System.out.println("狗尧来,线程执行了"+name);}public static void main(String[] args) {ThreadTest1 t1=new ThreadTest1(".png@466w_260h.png","1.png");ThreadTest1 t2=new ThreadTest1(".png@2560w_500h.png","2.png");ThreadTest1 t3=new ThreadTest1(".png@466w_260h.png","3.png");//多个线程同时进行,谁先下完谁最先打印t2.start();t1.start();t3.start();}
}//下载器
class WebDownLoader{//下载方法public void downLoader(String url,String name,String savePath){try {URL url1=new URL(url);File file=new File(savePath+name);FileUtils.copyURLToFile(url1,file);//通过URL将图片保存在文件里面} catch (IOException e) {e.printStackTrace();System.out.println("狗尧来,这个方法有问题");}}
}

2.实现Runnable接口

2.1实现Runnable接口

2.2重写run()方法

2.3执行线程需要创建runnable接口实现类,调用start()方法

代码:

package com.xia.thread;public class ThreadTest2 implements Runnable{public void run() {for (int i=0;i<20;i++){System.out.println("狗尧来学Java"+i);}}public static void main(String[] args) {//创建Runnable接口的实现类ThreadTest2 test2 = new ThreadTest2();//创建线程对象,通过将runnable接口的实现类放入线程对象,来开启线程Thread thread = new Thread(test2);thread.start();}
}

例:模拟龟兔赛跑

package com.xia.thread;public class Race implements Runnable{private static String winner;public void run() {for(int i=1;i<=100;i++){//显示走了多少步System.out.println(Thread.currentThread().getName()+"-->"+i);//判断比赛是否结束boolean is=isGameOver(i);if(is){break;}}}Boolean isGameOver(int i){//如果有winner就返回trueif(winner!=null){return true;}if(i>=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();}
}

结果如下:

D:\Java\bin\java.exe "-javaagent:C:\Users\xj\Desktop\java学习\IntelliJ IDEA 2020.3.3\lib\idea_rt.jar=51317:C:\Users\xj\Desktop\java学习\IntelliJ IDEA 2020.3.3\bin" -Dfile.encoding=UTF-8 -classpath D:\Java\jre\lib\charsets.jar;D:\Java\jre\lib\deploy.jar;D:\Java\jre\lib\ext\access-bridge-32.jar;D:\Java\jre\lib\ext\cldrdata.jar;D:\Java\jre\lib\ext\dnsns.jar;D:\Java\jre\lib\ext\jaccess.jar;D:\Java\jre\lib\ext\jfxrt.jar;D:\Java\jre\lib\ext\localedata.jar;D:\Java\jre\lib\ext\nashorn.jar;D:\Java\jre\lib\ext\sunec.jar;D:\Java\jre\lib\ext\sunjce_provider.jar;D:\Java\jre\lib\ext\sunmscapi.jar;D:\Java\jre\lib\ext\sunpkcs11.jar;D:\Java\jre\lib\ext\zipfs.jar;D:\Java\jre\lib\javaws.jar;D:\Java\jre\lib\jce.jar;D:\Java\jre\lib\jfr.jar;D:\Java\jre\lib\jfxswt.jar;D:\Java\jre\lib\jsse.jar;D:\Java\jre\lib\management-agent.jar;D:\Java\jre\lib\plugin.jar;D:\Java\jre\lib\resources.jar;D:\Java\jre\lib\rt.jar;C:\Users\xj\IdeaProjects\Jedis01\target\classes;C:\Users\xj\.m2\repository\redis\clients\jedis\3.2.0\jedis-3.2.0.jar;C:\Users\xj\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;C:\Users\xj\.m2\repository\org\apache\commons\commons-pool2\2.6.2\commons-pool2-2.6.2.jar;C:\Users\xj\.m2\repository\com\alibaba\fastjson\1.2.62\fastjson-1.2.62.jar;C:\Users\xj\.m2\repository\commons-io\commons-io\2.4\commons-io-2.4.jar com.xia.thread.Race
狗宗弟-->1
狗尧来-->1
狗宗弟-->2
狗尧来-->2
狗宗弟-->3
狗尧来-->3
狗宗弟-->4
狗尧来-->4
狗宗弟-->5
狗尧来-->5
狗宗弟-->6
狗尧来-->6
狗宗弟-->7
狗尧来-->7
狗尧来-->8
狗宗弟-->8
狗尧来-->9
狗宗弟-->9
狗尧来-->10
狗宗弟-->10
狗尧来-->11
狗宗弟-->11
狗尧来-->12
狗尧来-->13
狗尧来-->14
狗宗弟-->12
狗尧来-->15
狗宗弟-->13
狗尧来-->16
狗宗弟-->14
狗尧来-->17
狗尧来-->18
狗尧来-->19
狗尧来-->20
狗宗弟-->15
狗尧来-->21
狗尧来-->22
狗宗弟-->16
狗尧来-->23
狗宗弟-->17
狗尧来-->24
狗宗弟-->18
狗尧来-->25
狗尧来-->26
狗尧来-->27
狗宗弟-->19
狗尧来-->28
狗宗弟-->20
狗尧来-->29
狗宗弟-->21
狗尧来-->30
狗宗弟-->22
狗尧来-->31
狗宗弟-->23
狗尧来-->32
狗宗弟-->24
狗尧来-->33
狗宗弟-->25
狗尧来-->34
狗宗弟-->26
狗尧来-->35
狗宗弟-->27
狗宗弟-->28
狗尧来-->36
狗宗弟-->29
狗尧来-->37
狗尧来-->38
狗尧来-->39
狗宗弟-->30
狗尧来-->40
狗尧来-->41
狗尧来-->42
狗宗弟-->31
狗尧来-->43
狗尧来-->44
狗尧来-->45
狗尧来-->46
狗尧来-->47
狗宗弟-->32
狗尧来-->48
狗宗弟-->33
狗宗弟-->34
狗宗弟-->35
狗尧来-->49
狗宗弟-->36
狗尧来-->50
狗宗弟-->37
狗尧来-->51
狗宗弟-->38
狗宗弟-->39
狗宗弟-->40
狗宗弟-->41
狗宗弟-->42
狗宗弟-->43
狗宗弟-->44
狗尧来-->52
狗宗弟-->45
狗宗弟-->46
狗尧来-->53
狗宗弟-->47
狗尧来-->54
狗尧来-->55
狗宗弟-->48
狗尧来-->56
狗宗弟-->49
狗尧来-->57
狗尧来-->58
狗宗弟-->50
狗尧来-->59
狗尧来-->60
狗宗弟-->51
狗尧来-->61
狗宗弟-->52
狗尧来-->62
狗宗弟-->53
狗尧来-->63
狗宗弟-->54
狗尧来-->64
狗宗弟-->55
狗尧来-->65
狗宗弟-->56
狗尧来-->66
狗宗弟-->57
狗尧来-->67
狗宗弟-->58
狗尧来-->68
狗宗弟-->59
狗尧来-->69
狗宗弟-->60
狗尧来-->70
狗宗弟-->61
狗尧来-->71
狗尧来-->72
狗宗弟-->62
狗尧来-->73
狗宗弟-->63
狗尧来-->74
狗宗弟-->64
狗尧来-->75
狗宗弟-->65
狗尧来-->76
狗宗弟-->66
狗尧来-->77
狗宗弟-->67
狗尧来-->78
狗宗弟-->68
狗尧来-->79
狗宗弟-->69
狗尧来-->80
狗宗弟-->70
狗尧来-->81
狗宗弟-->71
狗宗弟-->72
狗尧来-->82
狗宗弟-->73
狗宗弟-->74
狗尧来-->83
狗宗弟-->75
狗尧来-->84
狗宗弟-->76
狗尧来-->85
狗宗弟-->77
狗尧来-->86
狗宗弟-->78
狗尧来-->87
狗宗弟-->79
狗尧来-->88
狗宗弟-->80
狗尧来-->89
狗宗弟-->81
狗尧来-->90
狗宗弟-->82
狗尧来-->91
狗尧来-->92
狗宗弟-->83
狗尧来-->93
狗尧来-->94
狗尧来-->95
狗宗弟-->84
狗宗弟-->85
狗宗弟-->86
狗尧来-->96
狗尧来-->97
狗宗弟-->87
狗尧来-->98
狗宗弟-->88
狗尧来-->99
狗宗弟-->89
狗尧来-->100
狗宗弟-->90
winner is 狗尧来Process finished with exit code 0

以上两种方式的比较:

继承 Thread 类

1.子类继承 Thread 类具备多线程能力

  • 启动线程:子类对象 .start()
  • 不建议使用:避免 OOP 单继承局限性

2.实现 Runnable 接口

  • 实现接口 Runnable 具有多线程能力
  • 启动线程:传入目标对象+Thread对象.start()
  • 推荐使用:避免单继承局限性,方便同一个对象被多个线程使用。

3.实现Callable接口

1.实现Calleble接口

2.重写call()方法

3.创建执行服务

ExecutorService ser = Executors.newFixedThreadPool(线程数);

提交执行

Future<返回值类型> r = ser.submit(线程名);

获取结果

类型 res=r.get();

关闭服务

ser.shutdownNow();

总结:

例如:

package com.xia.Thread;import java.util.concurrent.*;/*** @author: xjszsd* @date: 2021/12/09 11:20* @description:changsha*/public class ThreadByCallable implements Callable<Boolean> {//    网络图片地址private String url;//    保存的文件名private String name;public ThreadByCallable(String url,String name){this.url = url;this.name = name;}@Overridepublic Boolean call() throws Exception {//        进入线程后,会创建一个下载器,下载器通过 downloader 方法,传入 url 和 name 下载相应的资源WebDownloader webDownloader = new WebDownloader();webDownloader.downloader(url,name);System.out.println("下载了文件名为:"+ name);return true;}public static void main(String[] args) throws ExecutionException, InterruptedException {//        这是 TestThread2 类的主方法
//        创建三个继承 Thread 的子类ThreadByCallable test01 = new ThreadByCallable("=3796445054,4193265240&fm=26&gp=0.jpg", "test01");ThreadByCallable test02 = new ThreadByCallable(";quality=80&size=b9999_10000&sec=1605454961548&di=c3b49cc5869f058a6cded1434ea56f85&imgtype=0&src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F5%2F538ec3134b63b.jpg", "test02");ThreadByCallable test03 = new ThreadByCallable("=2044644877,1766802492&fm=15&gp=0.jpg", "test03");//        创建执行服务:ExecutorService service = Executors.newFixedThreadPool(3);//       提交执行Future<Boolean> submit01 = (Future<Boolean>) service.submit(test01);Future<Boolean> submit02 = (Future<Boolean>) service.submit(test02);Future<Boolean> submit03 = (Future<Boolean>) service.submit(test03);boolean rs1 = submit01.get();boolean rs2 = submit02.get();boolean rs3 = submit03.get();//        关闭服务service.shutdownNow();}
}//下载器,这是一个类
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("狗尧来,方法出现问题");}}
}

优点:1.可以定义返回值;2.能抛出异常

​ 缺点:相比而言较为麻烦

4.Lamda 表达式

Lamda 表达式属于函数式编程的概念

(paraems) -> expressionp[表达式]
(params) -> statement[语句]
(params) -> {statements}
a->System.out.println("i like lamda-->"+a);
他们的前面都是有对象接受的
new Thread(()->System.out.println("多线程学习...")).start();
  • 理解 Functional Interface(函数式接口)是学习 Java8 Lambda 表达式的关键所在。

  • 函数式接口的定义:

    • 任何接口,如果只包含唯一一个抽象方法,那么它就是函数式接口。
    • 对于函数式接口,可以通过 Lamda 表达式来创建该接口的对象。

Lamda 表达式的演进:

package com.sjmp.demo02;/*** @author: xjszsd* @date: 2021/12/9 20:11* @description:*/public class LamdaExpression {
//3.2 实现函数式接口的第二种方法,静态内部类,
//如果不加static,则为成员内部类,需要先创建外部类在创建内部类。static class Like2 implements ILike{@Overridepublic void lamda() {System.out.println("------3.2 静态内部类实现函数式接口-----");}
}public static void main(String[] args) {//    3.1 实现函数式接口的第一种方法ILike like1 = new Like1();like1.lamda();System.out.println("--3.1 普通方法实现函数式接口--");//    3.2 实现函数式接口的第二种方法,静态内部类new Like2().lamda();//    3.3 局部内部类实现函数式接口class Like3 implements ILike{@Overridepublic void lamda() {System.out.println("------3.3 局部内部类实现函数式接口--------");}}new Like3().lamda();//    3.4 匿名内部类实现函数式接口new ILike() {@Overridepublic void lamda() {System.out.println("------3.4 匿名内部类实现函数式接口----------");}}.lamda();//      3.5 lamda 表达式实现函数式接口ILike like5 = ()->{System.out.println("--3.5 lamda 表达式实现函数式接口--");};like5.lamda();}}// 1. 定义一个函数式接口
interface ILike{void lamda();
}// 2. 实现类
class Like1 implements ILike{@Overridepublic void lamda() {}
}

5.静态代理模式

多线程 Thread 为代理,Runnable 为被代理对象:

package com.sjmp.demo02;/*** @author: xjszsd* @date: 2021/12/9 19:32* @description:*///这是代理
public class StaticProxy implements Marry{private Marry you;public StaticProxy(You you){this.you = you;}public static void main(String[] args) {//        Runnable 是被代理的对象,Thread 是代理/*new Thread(new Runnable() {@Overridepublic void run() {System.out.println("----结婚----");}}).start();*///        使用 lamda 表达式new Thread(()->{System.out.println("----结婚----");}).start();new StaticProxy(new You()).HappyMarry();}@Overridepublic void HappyMarry() {doBefore();you.HappyMarry();doAfter();}public static void doBefore(){System.out.println("-----婚前布置------");}public static void doAfter(){System.out.println("-------婚后收钱-----");}}interface Marry{void HappyMarry();
}
//真实角色,Marryclass You implements Marry{@Overridepublic void HappyMarry() {System.out.println("----- 被代理人 开始结婚------");}
}

4.线程的 5 种状态

4.1 线程的一些常用方法

线程的一些方法如下图所示:

4.1.1 线程休眠——sleep()

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

sleep() 方法的用处
例如:

package com.xia.method;import java.awt.*;
import java.text.SimpleDateFormat;
import java.util.Date;import static java.lang.Thread.*;/*** @author: xjszsd* @date: 2021/12/10 21:50* @description:*/public class TestSleep {public static void main(String[] args) throws InterruptedException {
//        Thread.sleep()  用于倒计时//        tenStop();//        打印当前系统时间Date date = new Date(System.currentTimeMillis());boolean flag = true;int i = 5;while(flag){if(--i<=0){flag = false;}try {sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));date = new Date(System.currentTimeMillis());//更新时间}}//    写一个倒计时的方式public static void tenStop() throws InterruptedException {int num = 10;while(true){try{sleep(1000);}catch ( InterruptedException e){e.printStackTrace();}if (num<=0){break;}System.out.println(num--);}}
}

4.1.2 线程礼让——yield()

  • 礼让线程:让当前正在执行的线程暂停,但不阻塞;
  • 将线程从运行状态转为就绪状态;
  • 让 CPU 从新调度,有可能还是调度该礼让线程。
package com.xia.method;/*** @author: xjszsd* @date: 2021/12/10 22:22* @description:*/public class TestYield implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"开启了线程");Thread.yield();System.out.println(Thread.currentThread().getName()+"结束了线程");}public static void main(String[] args) {TestYield testYield = new TestYield();Thread threadA = new Thread(testYield,"threadA");Thread threadB = new Thread(testYield,"threadB");threadA.start();threadB.start();}}

4.1.2 合并线程——Join()

Join 合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞,可以想象成插队。

package com.xia.method;public class TestJoin implements Runnable{@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println("Thread : "+i);}}public static void main(String[] args) throws InterruptedException {TestJoin join = new TestJoin();Thread thread = new Thread(join);thread.start();for (int i = 0; i < 200; i++) {if (i==100){thread.join();}System.out.println("main :"+i);}}
}

4.2 停止线程的方式

  • 不推荐使用 JDK 提供的 stop ()、destroy()方法。【已弃用】
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量 , 当 flag == false,则终止线程运行。
package com.xia.demo02;public class ThreadStop implements Runnable{private boolean flag = true;@Overridepublic void run() {int i = 0;while (flag){System.out.println("--- ThreadStop  ---"+i);i++;}}public void stop(){this.flag = false;}public static void main(String[] args) {ThreadStop threadStop = new ThreadStop();Thread thread = new Thread(threadStop);thread.start();for (int i = 0; i < 1000; i++) {if (i==900){threadStop.stop();}System.out.println("--- main ---"+i);}}}

4.3 线程状态观测

Thread.State
线程状态。线程可以处于以下状态之一:

  • NEW
    尚未启动的线程处于此状态。
  • RUNNABLE
    在 Java 虚拟机中执行的线程处于此状态。
  • BLOCKED
    被阻塞等待监视器锁定的线程处于此状态。
  • WAITING
    正在等待另一个线程执行特定动作的线程处于此状态。
  • TERMINATED
    已退出的线程处于此状态。
    一个线程可以在给定时间点处于一个状态。
package com.xia.method;public class TestState{public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(()->{for (int i = 0; i < 20; i++) {try {System.out.println(i);Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("Thread 执行结束了!");});Thread.State state = thread.getState();System.out.println(state);thread.start();System.out.println(thread.getState());System.out.println(" 我开始循环了 ");while(state != Thread.State.TIMED_WAITING){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}state = thread.getState();System.out.println(state);}}
}

4.4 线程优先级

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

  • 线程的优先级用数字表示,范围从1~10.

    Thread.MIN_PRIORITY=1;
    Thread.MAX_PRIORITY=10;
    Thread.NORM_PRIORITY=5;

  • 使用以下方式改变或获取优先级

    getPriority().setPriority(int xxx)

优先级的设定建议在 start() 调度前

package com.xia.method;public class TestPriority implements Runnable {@Overridepublic void run() {System.out.println("当前线程:"+Thread.currentThread().getName());}public static void main(String[] args) {TestPriority runnable = new TestPriority();Thread thread01 = new Thread(runnable,"01");Thread thread02 = new Thread(runnable,"02");Thread thread03 = new Thread(runnable,"03");Thread thread04 = new Thread(runnable,"04");Thread thread05 = new Thread(runnable,"05");thread01.setPriority(Thread.MAX_PRIORITY);thread02.setPriority(7);thread03.setPriority(6);thread04.setPriority(5);thread05.setPriority(4);thread01.start();thread02.start();thread03.start();thread04.start();thread05.start();}
}

4.5 守护(daemon)线程

线程分为用户线程守护线程

虚拟机必须确保用户线程执行完毕
虚拟机不用等待守护线程执行完毕

如,后台记录操作日志,监控内存垃圾回收等待…

package com.xia.method;public class TestsetDaemon {public static void main(String[] args) {God god = new God();You you = new You();Thread thread = new Thread(god);thread.setDaemon(true);thread.start();new Thread(you).start();}
}
class God implements Runnable{@Overridepublic void run() {while (true){System.out.println("Daemoning...");}}
}class You implements Runnable{@Overridepublic void run() {for (int i = 0; i < 365; i++) {System.out.println("living...");}System.out.println("game over---------------------");}
}

4.6 并发

同一个对象多个线程同时操作

现实生活中,我们会遇到”同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队一个个来。

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象.这时候我们就需要线程同步.线程同步其实就是一种等待机制,多个需要同时访问!此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。

形成线程安全的条件:队列和锁

5.线程同步

由于同一进程多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可.存在以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起;
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题.

5.1 线程不安全举例

举例:不安全的售票

package com.xia.Concurrent;public class UnsafeBuyTicket {}class BuyTicket implements Runnable{private int ticketNum = 10;boolean  flag = true;@Overridepublic void run() {while (flag){buy();}System.out.println("售罄");}//加关键字 synchronized 就可以变成线程安全的public void buy(){if (ticketNum<=0){flag = false;return;}try {//            模拟买票延时Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+" get ticket:"+ticketNum);ticketNum--;}public static void main(String[] args) {BuyTicket buyTicket = new BuyTicket();Thread thread01 = new Thread(buyTicket,"01");Thread thread02 = new Thread(buyTicket,"02");Thread thread03 = new Thread(buyTicket,"03");thread01.start();thread02.start();thread03.start();}
}

举例:
线程不安全的集合
可参考:ArrayList为什么是线程不安全的

package com.xia.Concurrent;import java.util.ArrayList;public class UnsafeList {public static void main(String[] args) {ArrayList<String> list = new ArrayList<>();for (int i = 0; i < 10000; i++) {new Thread(()->{list.add(Thread.currentThread().getName());}).start();}try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(list.size());}
}

举例:
线程安全的集合:CopyOnWriteArrayList

package com.xia.Concurrent;import java.util.concurrent.CopyOnWriteArrayList;public class TestJUC {public static void main(String[] args) {CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();for (int i = 0; i < 1000; i++) {new Thread(()->{list.add(Thread.currentThread().getName());}).start();}try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(list.size());}
}

5.2 同步方法

由于我们可以通过关键字 private 关键字来保证数据对象只能被方法访问,所以我们只要针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种方法:
synchronized 方法和 synchronized 块.

同步方法:

public synchronized void method(int args){}

synchronized 方法控制 “对象” 的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放,后面被阻塞的线程才能获得这个锁,继续执行。

缺陷:若将一个大的方法申明为 synchronized 将会影响效率。

5.3 同步块

同步块:synchronized(Obj){}
Obj 称之为同步监视器

  • Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是 this ,就是这个对象本身,或者是 class
    同步监视器的执行过程

1.第一个线程访问,锁定同步监视器,执行其中的代码
2.第二个线程访问,发现同步监视器被锁定,无法访问
3.第一个线程访问完毕,解锁同步监视器
4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问

5.4 死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥“两个以上对象的锁”时,就可能会发生“死锁”的问题。

产生死锁的四个必要条件:

互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上述四个条件,只要破坏其任意一个就可避免死锁的发生。

5.5 Lock(锁)

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

synchronized 与 Lock 的对比

  • Lock 是显示锁(手动开启和关闭),synchronized 是隐式锁,出了作用域自动释放
  • Lock 只有代码加锁,synchronized 有代码块锁和方法锁
    使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并具有更好的扩展性(提供更多的子类)
  • Lock > 同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
package com.xia.Concurrent;import java.util.concurrent.locks.ReentrantLock;public class TestLock {public static void main(String[] args) {Ticket ticket = new Ticket();new Thread(ticket).start();new Thread(ticket).start();new Thread(ticket).start();}}class Ticket extends Thread{private int ticketNums = 10;
//    定义 lock 锁private final ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while(true){try{lock.lock();if (ticketNums>0){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(ticketNums--);}else{break;}}finally {lock.unlock();}}}
}

6.线程通信

应用场景:生产者和消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。

  • 如果仓库中没有产品,则将生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。

  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费,直到仓库中再次放入产品为止。

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

  • 对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后,又需要马上通知消费者消费
  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费.
  • 在生产者消费者问题中,仅有synchronized是不够的

synchronized 可阻止并发更新同一个共享资源,实现了同步
synchronized 不能用来实现不同线程之间的消息传递(通信)

6.1 解决线程之间通信问题的几个方法

注意:均是 Object 类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常 llegalMonitorStateException

sleep 与 wait 的区别

6.2 解决线程之间通信的方式1:管程法

并发写作模型“生产者/消费者模式”–>管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程);
  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个缓冲区

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

package com.xia.advanced;// 生产者,消费者,产品,缓冲区
public class TestPC {public static void main(String[] args) {SynContainer container = new SynContainer();new Productor(container).start();new Consumer(container).start();}}// 生产者
class Productor extends Thread{SynContainer container;public Productor(SynContainer container){this.container = container;}@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("生产了"+i+"只鸡");container.push(new Chicken(i));}}
}class SynContainer{
//    需要一个容器的大小Chicken[] chickens = new Chicken[10];
//    容器计数器int count = 0;//    生产者放入产品public synchronized void push(Chicken chicken){
//        如果容器满了,就需要等待消费者消费if (count == chickens.length){
//            通知消费者消费,生产等待try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}chickens[count] = chicken;count++;this.notifyAll();}//    消费者消费产品public synchronized Chicken pop(){
//        判断能否消费if(count==0){
//            等待生产者生产,消费者等待try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}
//        如果可以消费count--;Chicken chicken = chickens[count];
//        可以通知消费了this.notifyAll();return chicken;}
}class Consumer extends Thread{SynContainer container;public Consumer(SynContainer container){this.container = container;}@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("消费了-->"+container.pop().id+"只鸡");}}
}// 产品
class Chicken{int id;  //产品编号public Chicken(int id){this.id = id;}
}

6.3 解决线程之间通信的方式1:信号灯法

package com.xia.advanced;public class TestPC2 {public static void main(String[] args) {TV tv = new TV();new Player(tv).start();new Wathcher(tv).start();}
}//生产者--演员
class Player extends Thread{TV tv;public Player(TV tv){this.tv = tv;}@Overridepublic void run() {for (int i = 0; i < 20; i++) {if(i%2==0){this.tv.play("快乐大本营");}else{this.tv.play("天天向上");}}}
}//观众
class Wathcher extends Thread{TV tv;public Wathcher(TV tv){this.tv = tv;}@Overridepublic void run() {for (int i = 0; i < 20; i++) {tv.watch();}}
}//产品--节目
class TV{
//    演员表演,观众等待  T
//    观众观看,演员等待  FString voice;  // 表演节目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.voice = voice;this.flag = !flag;}//    观看public synchronized void watch(){if (flag){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("观看了: "+voice);
//        通知演员表演this.notifyAll();this.flag = !this.flag;}
}

6.4 使用线程池

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

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

优点
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理…

corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间会终止

package com.sjmp.advanced;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TestPool {public static void main(String[] args) {
//        1.创建服务,创建线程池ExecutorService service = Executors.newFixedThreadPool(10);
//        newFixedThreadPool 参数为线程池大小
//        执行service.execute(new MyThread());service.execute(new MyThread());service.execute(new MyThread());service.execute(new MyThread());//        2.关闭连接service.shutdown();}
}
class MyThread implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}
}

7. 补充内容

package com.xia.Thread01;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @ClassName ThreadFutureTest* @Description TODO* @Author sjmp1573* @Date DATE{TIME}*/
public class ThreadFutureTest {public static class CallerTask implements Callable<String>{@Overridepublic String call() throws Exception {return "Thread-Callable- hello";}}public static void main(String[] args) {// 创建异步任务FutureTask<String> futureTask = new FutureTask<>(new CallerTask());new Thread(futureTask).start();try {String result = futureTask.get();System.out.println(result);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}

如上代码中的 CallerTask 类实现了 Callable 接口的 call() 方法。在 main 函数内首先创建了一个 FutrueTask 对象(构造函数为 CallerTask 的实例),然后使用创建的 FutrueTask 对象作为任务创建了一个线程并且启动它,最后通过futureTask.get() 等待任务执行完毕并返回结果。

小结:使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递,而如果使用 Runnable 方式,则只能使用主线程里面被声明为 final 的变量。不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么子类不能再继承其他类,而 Runable 则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是 Futuretask 方式可以。

原文链接:

本文标签: 狂神说多线程笔记