学习视频参考链接:https://www.bilibili.com/video/BV1V4411p7EF?p=27

  • 线程简介
  • 线程的实现(重点)
  • 线程状态
  • 线程同步(重点)
  • 线程通信问题
  • 高级主题(重点)

文章目录

    • 1、线程简介
    • 2、线程实现
    • 2.1 继承 Thread 类,重写 run 方法
    • 2.2 继承 Runnable 接口,创建 Tread 对象
    • 2.3 实现 Callable 接口(了解)
    • 2.4 Lamda 表达式
    • 2.5 静态代理模式
  • 3、线程的 5 种状态
    • 3.1 线程的一些常用方法
      • 3.1.1 线程休眠——sleep()
      • 3.1.2 线程礼让——yield()
      • 3.1.2 合并线程——Join()
    • 3.2 停止线程的方式
    • 3.3 线程状态观测
    • 3.4 线程优先级
    • 3.5 守护(daemon)线程
    • 3.6 并发
  • 4、线程同步
    • 4.1 线程不安全举例
    • 4.2 同步方法
    • 4.3 同步块
    • 4.3 死锁
    • 4.4 Lock(锁)
  • 5.线程通信
    • 5.1 解决线程之间通信问题的几个方法
    • 5.2 解决线程之间通信的方式1:管程法
    • 5.3 解决线程之间通信的方式1:信号灯法
    • 5.4 使用线程池
    • 6 补充内容
      • 6.1 wait() 方法

1、线程简介

栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过 JVM 的启动参数来进行调整,栈空间用光了会引发 StackOverflowError,而堆和常量池空间不足则会引发 OutOfMemoryError。

String str = new String("hello");

上面的语句中变量 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而 “hello” 这个字面量是放在方法区的。


例子:

开车 + 打电话

吃饭 + 玩手机

这些动作都可以抽象为任务,虽然看起来一心二用,但人只有一个大脑,在一个时间片刻只能处理一个任务。

CPU 也是一样,面对多个任务,只能在一个时间片刻处理一个任务。

主线程调用 run 方法和调用 start 方法开启子线程的区别如下图所示。

  • 线程就是独立的执行路径;
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,GC 线程;
  • main()称之为主线程,为系统的入口,用于执行整个程序;
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预。
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
  • 线程会带来额外的开销,如 CPU 调度时间,并发控制开销。
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

2、线程实现

线程的三种实现方式:

2.1 继承 Thread 类,重写 run 方法

继承 Thread 类,重写 run 方法。创建这个类的对象,再调用 start() 即可

package com.sjmp.Thread01;/*** @ClassName ThreadTest* @Description TODO* @Author sjmp1573* @Date DATE{TIME}*/
public  class ThreadTest {// 继承 Thread 类并重写 run 方法public static class MyThread extends Thread{@Overridepublic void run() {System.out.println("I am a child thread");}}public static void main(String[] args) {//创建一个线程MyThread thread = new MyThread();//启动线程thread.start();}
}

下载文件需要在 pom.xml 中 commons io 包。

使用该方法下载网络图片。

package com.sjmp.demo01;import org.apache.commons.io.FileUtils;import java.io.File;
import java.io.IOException;
import java.net.URL;/*** @author: sjmp1573* @date: 2020/11/15 20:58* @description:*/public class TestThread2 extends Thread{//    网络图片地址private String url;
//    保存的文件名private String name;public TestThread2(String url,String name){this.url = url;this.name = name;}@Overridepublic void run() {//        进入线程后,会创建一个下载器,下载器通过 downloader 方法,传入 url 和 name 下载相应的资源WebDownloader webDownloader = new WebDownloader();webDownloader.downloader(url,name);System.out.println("下载了文件名为:"+ name);}public static void main(String[] args) {//        这是 TestThread2 类的主方法
//        创建三个继承 Thread 的子类TestThread2 test01 = new TestThread2("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796445054,4193265240&fm=26&gp=0.jpg", "test01");TestThread2 test02 = new TestThread2("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1605454961548&di=c3b49cc5869f058a6cded1434ea56f85&imgtype=0&src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F5%2F538ec3134b63b.jpg", "test02");TestThread2 test03 = new TestThread2("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2044644877,1766802492&fm=15&gp=0.jpg", "test03");//        并开启线程test01.start();test02.start();test03.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异常,downloader 方法出现问题");}}
}
        <!-- https://mvnrepository.com/artifact/commons-io/commons-io --><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.8.0</version></dependency>

2.2 继承 Runnable 接口,创建 Tread 对象

继承 Runnable 接口,创建 Tread 对象,传入实现类,开启 start 方法.

package com.sjmp.Thread01;/*** @ClassName ThreadRunnableTest* @Description TODO* @Author sjmp1573* @Date DATE{TIME}*/
public class ThreadRunnableTest {public static class MyThread implements Runnable {@Overridepublic void run() {System.out.println("I am a child thread --Runnable");}}public static void main(String[] args) {MyThread thread = new MyThread();new Thread(thread).start();new Thread(thread).start();}
}

以上两种方式的比较:

继承 Thread 类

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

实现 Runnable 接口

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

火车抢票实例:

Runnable 实现多线程,创造一个实列 ticketRunnable ,可共享给多个线程。

package com.sjmp.demo01;/*** @author: sjmp1573* @date: 2020/11/15 21:45* @description:*/// 多个线程同时操作同一个对象
//    买火车票的例子
//    发现问题:多个线程操作同一个资源,线程不安全,数据紊乱!public class TicketRunnable implements Runnable{private int ticketNums = 10;@Overridepublic void run() {while (true){if (ticketNums<=0){break;}
//            模拟延时/*IllegalArgumentExceptionif the value of {@code millis} is negative, or the value of{@code nanos} is not in the range {@code 0-999999}InterruptedExceptionif any thread has interrupted the current thread. The<i>interrupted status</i> of the current thread iscleared when this exception is thrown.*/try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}ticketNums--;System.out.println(Thread.currentThread().getName()+"-->拿到了第"+ticketNums+"票");}}public static void main(String[] args) {//        实现了 Runnable 接口的类,创建其实例TicketRunnable ticketRunnable = new TicketRunnable();
//        ticketRunnable 实例可用于多个线程,其中的资源被共享。new Thread(ticketRunnable,"01小明+++++").start();new Thread(ticketRunnable,"02老师-----").start();new Thread(ticketRunnable,"03黄牛=====").start();}
}
"D:\Program Files (x86)\Java\bin\java.exe" "-javaagent:D:\Program Files (x86)\IDEA\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=64250:D:\Program Files (x86)\IDEA\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath "D:\Program Files (x86)\Java\jre\lib\charsets.jar;D:\Program Files (x86)\Java\jre\lib\deploy.jar;D:\Program Files (x86)\Java\jre\lib\ext\access-bridge-64.jar;D:\Program Files (x86)\Java\jre\lib\ext\cldrdata.jar;D:\Program Files (x86)\Java\jre\lib\ext\dnsns.jar;D:\Program Files (x86)\Java\jre\lib\ext\jaccess.jar;D:\Program Files (x86)\Java\jre\lib\ext\jfxrt.jar;D:\Program Files (x86)\Java\jre\lib\ext\localedata.jar;D:\Program Files (x86)\Java\jre\lib\ext\nashorn.jar;D:\Program Files (x86)\Java\jre\lib\ext\sunec.jar;D:\Program Files (x86)\Java\jre\lib\ext\sunjce_provider.jar;D:\Program Files (x86)\Java\jre\lib\ext\sunmscapi.jar;D:\Program Files (x86)\Java\jre\lib\ext\sunpkcs11.jar;D:\Program Files (x86)\Java\jre\lib\ext\zipfs.jar;D:\Program Files (x86)\Java\jre\lib\javaws.jar;D:\Program Files (x86)\Java\jre\lib\jce.jar;D:\Program Files (x86)\Java\jre\lib\jfr.jar;D:\Program Files (x86)\Java\jre\lib\jfxswt.jar;D:\Program Files (x86)\Java\jre\lib\jsse.jar;D:\Program Files (x86)\Java\jre\lib\management-agent.jar;D:\Program Files (x86)\Java\jre\lib\plugin.jar;D:\Program Files (x86)\Java\jre\lib\resources.jar;D:\Program Files (x86)\Java\jre\lib\rt.jar;E:\SJMP\SpringProject\out\production\LeetCode" com.sjmp.TicketRunnable
03黄牛=====-->拿到了第8票
01小明+++++-->拿到了第8票
02老师------->拿到了第8票
03黄牛=====-->拿到了第7票
02老师------->拿到了第7票
01小明+++++-->拿到了第7票
02老师------->拿到了第6票
03黄牛=====-->拿到了第6票
01小明+++++-->拿到了第6票
01小明+++++-->拿到了第5票
03黄牛=====-->拿到了第4票
02老师------->拿到了第5票
03黄牛=====-->拿到了第3票
02老师------->拿到了第3票
01小明+++++-->拿到了第3票
02老师------->拿到了第1票
03黄牛=====-->拿到了第2票
01小明+++++-->拿到了第2票
02老师------->拿到了第0票
01小明+++++-->拿到了第-1票
03黄牛=====-->拿到了第-1票Process finished with exit code 0

2.3 实现 Callable 接口(了解)

  1. 实现 Callable 接口,需要返回值类型
  2. 重写 call 方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService = Executor.newFixedThreadPool(1);
  5. 提交执行:Future result1 = ser.submit(1);
  6. 获取结果:boolean r1 = result.get()
  7. 关闭服务:ser.shutdownNow():
package com.sjmp.demo01;import java.util.concurrent.*;/*** @author: sjmp1573* @date: 2020/11/15 22:16* @description:*/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("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796445054,4193265240&fm=26&gp=0.jpg", "test01");ThreadByCallable test02 = new ThreadByCallable("https://timgsa.baidu.com/timg?image&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("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=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("IO异常,downloader 方法出现问题");}}
}

--------------------------------------- 华丽的分割线 ----------------------------------------

以下代码来源于《Java 并发编程之美》

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

public class FutureTask<V> implements RunnableFuture<V>{}public interface RunnableFuture<V> extends Runnable, Future<V>
package com.sjmp.practice;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @author: sjmp1573* @date: 2021/4/20 21:12* @description:*/public class CallableTest implements Callable<String> {@Overridepublic String call() throws Exception {return "Hello CallableThread";}public static void main(String[] args) {CallableTest callableTest = new CallableTest();FutureTask<String> futureTask = new FutureTask<>(callableTest);new Thread(futureTask).start();try {String s = futureTask.get();System.out.println(s);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}

Hello CallableThreadProcess finished with exit code 0

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: sjmp1573* @date: 2020/11/16 20:11* @description:*/public class LamdaExpression {//    3.2 实现函数式接口的第二种方法,静态内部类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() {}
}

2.5 静态代理模式

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

package com.sjmp.demo02;/*** @author: sjmp1573* @date: 2020/11/16 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("----- 被代理人 开始结婚------");}
}

3、线程的 5 种状态

  1. 创建
  2. 就绪
  3. 阻塞
  4. 运行
  5. 死亡

3.1 线程的一些常用方法

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

3.1.1 线程休眠——sleep()

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

sleep() 方法的用处

package com.sjmp.method;import java.awt.*;
import java.text.SimpleDateFormat;
import java.util.Date;import static java.lang.Thread.*;/*** @author: sjmp1573* @date: 2020/11/16 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--);}}
}

3.1.2 线程礼让——yield()

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞;
  • 将线程从运行状态转为就绪状态;
  • 让 CPU 从新调度,有可能还是调度该礼让线程。
package com.sjmp.method;/*** @author: sjmp1573* @date: 2020/11/16 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();}}

3.1.2 合并线程——Join()

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

package com.sjmp.method;/*** @author: sjmp1573* @date: 2020/11/17 20:46* @description:*/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);}}
}

3.2 停止线程的方式

  • 不推荐使用 JDK 提供的 stop ()、destroy()方法。【已弃用】
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量 , 当 flag == false,则终止线程运行。
package com.sjmp.demo02;/*** @author: sjmp1573* @date: 2020/11/16 21:33* @description:*/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);}}}

3.3 线程状态观测

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

  • NEW
    尚未启动的线程处于此状态。
  • RUNNABLE
    在 Java 虚拟机中执行的线程处于此状态。
  • BLOCKED
    被阻塞等待监视器锁定的线程处于此状态。
  • WAITING
    正在等待另一个线程执行特定动作的线程处于此状态。
  • TERMINATED
    已退出的线程处于此状态。
    一个线程可以在给定时间点处于一个状态。
package com.sjmp.method;/*** @author: sjmp1573* @date: 2020/11/17 21:10* @description:*/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);}}
}

3.4 线程优先级

  • Java 提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
  • 线程的优先级用数字表示,范围从1~10.
    Thread.MIN_PRIORITY=1;
    Thread.MAX_PRIORITY=10;
    Thread.NORM_PRIORITY=5;
  • 使用以下方式改变或获取优先级
    getPriority().setPriority(int xxx)

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

package com.sjmp.method;/*** @author: sjmp1573* @date: 2020/11/17 21:32* @description:*/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();}
}

3.5 守护(daemon)线程

  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如,后台记录操作日志,监控内存垃圾回收等待…
package com.sjmp.method;/*** @author: sjmp1573* @date: 2020/11/17 21:43* @description:*/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---------------------");}
}

3.6 并发

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


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

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


形成线程安全的条件:

队列和锁

4、线程同步

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

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

4.1 线程不安全举例

举例:不安全的售票

package com.sjmp.Concurrent;/*** @author: sjmp1573* @date: 2020/11/17 22:03* @description:*/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为什么是线程不安全的:https://blog.csdn.net/qq_42183409/article/details/100586255

package com.sjmp.Concurrent;import java.util.ArrayList;/*** @author: sjmp1573* @date: 2020/11/17 22:22* @description:*/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.sjmp.Concurrent;import java.util.concurrent.CopyOnWriteArrayList;/*** @author: sjmp1573* @date: 2020/11/18 9:48* @description:*/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());}
}

4.2 同步方法

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

同步方法:

public synchronized void method(int args){}
  • synchronized 方法控制 “对象” 的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放,后面被阻塞的线程才能获得这个锁,继续执行。
    缺陷:若将一个大的方法申明为 synchronized 将会影响效率。

4.3 同步块

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

  • Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是 this ,就是这个对象本身,或者是 class
  • 同步监视器的执行过程
  1. 第一个线程访问,锁定同步监视器,执行其中的代码
  2. 第二个线程访问,发现同步监视器被锁定,无法访问
  3. 第一个线程访问完毕,解锁同步监视器
  4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

4.3 死锁

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

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

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

上述四个条件,只要破坏其任意一个就可避免死锁的发生。

4.4 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.sjmp.Concurrent;import java.util.concurrent.locks.ReentrantLock;/*** @author: sjmp1573* @date: 2020/11/18 16:52* @description:*/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();}}}
}

5.线程通信

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

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

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

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

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

  • 对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后,又需要马上通知消费者消费
  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费.
  • 在生产者消费者问题中,仅有synchronized是不够的
    synchronized 可阻止并发更新同一个共享资源,实现了同步
    synchronized 不能用来实现不同线程之间的消息传递(通信)

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

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


sleep 与 wait 的区别可参考链接:https://www.xuexila.com/baikezhishi/537124.html

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

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

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

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

package com.sjmp.advanced;/*** @author: sjmp1573* @date: 2020/11/18 20:52* @description:*/// 生产者,消费者,产品,缓冲区
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;}
}

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

package com.sjmp.advanced;/*** @author: sjmp1573* @date: 2020/11/18 21:34* @description:*/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;}
}

5.4 使用线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

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

  • corePoolSize:核心池的大小
  • maximumPoolSize:最大线程数
  • keepAliveTime:线程没有任务时最多保持多长时间会终止
package com.sjmp.advanced;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @author: sjmp1573* @date: 2020/11/18 21:53* @description:*/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());}
}

6 补充内容

package com.sjmp.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 方式可以。

6.1 wait() 方法

虚假唤醒

Java 多线程学习笔记(狂神)相关推荐

  1. java多线程学习笔记。

    java多线程学习笔记 线程的优缺点: 多线程的好处: 充分利用多处理核心,提高资源的利用率和吞吐量. 提高接口的响应效率,异步系统工作. 线程的风险: 安全危险(竞争条件):什么坏事都没有发生.在没 ...

  2. Java入门学习笔记[狂神说Java]

    写在前面: 本文根据B站狂神说Java 与菜鸟教程 整理而来,仅供个人学习使用,如有侵权,请联系删除. 文章目录 IDEA使用 Java基础01:注释 Java基础02:数据类型 Java基础03:类 ...

  3. Java 多线程学习笔记

    概念 进程 正在运行的程序,是系统进行资源分配和调用的独立单位 每一个进程都有它自己的内存空间和系统资源,一个进程包括由操作系统分配的内存空间,包含一个或多个线程 一个进程一直运行,直到所有的非守护线 ...

  4. Java多线程学习笔记一

    一.关于多线程 Java中的多线程是一个同时执行多个线程的进程.线程是一个轻量级的子进程,是最小的处理单元.线程使用共享内存区域,不分配单独的内存区域以节省内存.Java多线程多用于游戏.动画方面. ...

  5. java多线程学习笔记--一.多线程的基础知识

    需要学习的知识 多线程基础知识讲解 参考索隆和jim的视频,以及自己做的笔记 导读 为了充分利用CPU资源,人们发明了线程和进程 进程 由来:在单核cpu的时期,为了方便操作把一系列的操作的指令写下来 ...

  6. 【JAVA多线程学习笔记】(1)实现线程的方式 线程生命周期 操作线程的方法

    文章目录 两种方式实现线程 继承Thread类 模拟银行叫号的程序 Runnable接口 代码1:(与swing相结合创建gui程序) Thread类的⼏个常⽤⽅法 线程生命周期 操作线程的方法 代码 ...

  7. Java多线程学习笔记-线程的使用

    Java中创建多线程的三种方法 1.继承Thread类创建线程 2.实现Runnable接口创建线程 3.使用Callable和Future创建线程 ------------------------- ...

  8. java 多线程学习笔记(二) -- IO密集型任务

    IO密集型是指对IO操作较多的任务.下面以查询一些股票价格任务为例: YahooFinance.java public class YahooFinance {public static double ...

  9. java多线程学习笔记(一)

                                       ✟  "In my Father's house are many mansions: if it were not s ...

最新文章

  1. VS.net 2005 试用(1)
  2. Vim删除文件到行首或者行尾
  3. mysql 索引计划_Mysql索引、查询计划、优化方向
  4. 经典逻辑编程题(本文用python实现)
  5. 利用Session实现一次性验证码(多学一招)
  6. ETL学习总结(1)——ETL 十大功能特性详解
  7. Bailian2745 显示器【打印图案】
  8. 华为交换机导入配置_华为交换机配置文件导入 华为s5700交换机配置教程
  9. 深度学习自学(二十六):人脸关键点检测
  10. leetcode 347
  11. C#中使用正则表达式验证电话号码、手机号、身份证号、数字、邮编、时间(仅年月日)、邮箱、小数的正则表达式...
  12. 小米小方摄像头云存储_小米摄像头离线?
  13. H.265和VP9视频编码要被干掉?解读全新AV1编码标准
  14. 关于网络连接里无虚拟网络适配器的解决办法
  15. P68-70 王者荣耀
  16. 怎么更改计算机管理员的用户名,如何修改系统默认账户administrator
  17. nRF24L01 无线数传模块之间的区别 干货分析
  18. Python函数的应用--汇率转换函数示例(第九章)
  19. 爬虫python下载网站所有图片_爬取某图片网站多页图片的python爬虫
  20. Arthas开源一周年,Github Star 16K,我们一直在坚持什么?

热门文章

  1. 实验四-哈夫曼编码的MATLAB实现
  2. 智能座舱全舱感知系统SCSS
  3. 土木工程与计算机专业考研学校排名,土木工程专业考研学校排名
  4. 使用 Bud and Go 构建全栈应用程序
  5. 物联网-The Internet of Things
  6. python爬取网站源代码+图片
  7. 基于antd pro框架,配置接口apiUrl前缀
  8. 光耦使用教程——以4N25为例
  9. 【论文笔记】基于范围的有障碍最近邻查询(RONN)
  10. 什么是类、什么是对象