JAVA多线程

JAVA多线程

目录

基本概念

创建线程

Lambda表达式

静态代理

线程的状态

守护线程

线程的同步机制

线程协作

线程池(还没写完,后续更新)

基本概念

  • 在我们操作系统中运行的程序就是进程,比如播放器,游戏等等......
  • 一个进程可以有多个线程,如视频中同时可以听声音,看图像,看弹幕等等.......
  • 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
  • 但是进程则是执行程序的一次执行过程,他是一个动态的概念。是系统资源分配的单位
  • 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。
  • 线程是CPU调度和执行的单位
  • 注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器等等......很多模拟出来的多线程实际上是一个cpu快速切换出来的错觉,其原因在于单个cpu只能在一个时间点执行一个代码,并没有同时进行两个代码及以上。
  • 在我们对同一份资源操作是4,会存在资源抢夺的问题,需要我们加入并发控制
  • 线程会带来额外的开销,如cpu调度事件,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

创建线程

  • 线程的三种创建方式:
    • 继承Thread类
    • 实现Runnable接口
    • 实现Callable接口

Thread的使用

  • 自定义线程类继承Thread类

  • 重写run()方法,编写线程的执行体

  • 创建线程对象,调用start()方法启动线程

  • 代码例子:

package Demo01;

//创建线程方式1:使用继承Thread类后重写run()方法,调用start方法开启线程


public class ThreadStudy extends Thread{
    @Override
    public void run(){
        //重写run方法,以下为线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("HIBIKI---"+i);
        }
    }

    public static void main(String[] args) {
        //main线程,主线程

        //创建一个线程的对象
        ThreadStudy tst=new ThreadStudy();
        //调用start()方法开启线程
        tst.start();

        for (int i = 0; i < 200; i++) {
            System.out.println("Ayanami---"+i);
        }
        //运行后在"Ayanami---"后面有时候会出现"HIBIKI---"的字符串,说明程序的多线程运行实现了,这两个线程是同时运行的
    }
}

  • 线程开启后不一定立即执行,需要由CPU调度才能执行,因此在顺序上可能与我们想象的会有所不同

Runnable的使用

  • 自定义类实现Runnable接口

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

  • 创建线程对象,调用start()方法启动线程

  • 代码例子

package Demo01;


//创建线程方式2:使用Runnable接口,并重写接口里的run方法,执行线程需要放入Runnable接口实现类。调用start方法
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        //重写run方法,以下为线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("HIBIKI---" + i);
        }
    }


    public static void main(String[] args) {
        //main线程,主线程
        //创建Runnable接口的实现类对象
        MyRunnable myRunnable = new MyRunnable();
        //创建线程对象,通过线程对象来开启我们的线程
        Thread thread = new Thread(myRunnable);   //将Runnable接口的实现类对象放入
        thread.start();      //启动线程

        //此处也可以直接使用语句: new Thread (myRunnable).start();
        
        for (int i = 0; i < 200; i++) {
            System.out.println("Ayanami---"+i);
        }
        //运行后在"Ayanami---"后面有时候会出现"HIBIKI---"的字符串,说明程序的多线程运行实现了,这两个线程是同时运行的
}
}

  • 实现Runnable接口来进行多线程比继承Thread类的好处就是:
    • 子类继承Thread具有单继承的局限性,会导致无法继承其他类
    • 避免了单继承的局限性,方便同一个对象被多个线程使用

Callable接口的使用

  • 使用方法:

    1. 实现Callable接口,需要返回值的类型
    2. 重写call方法,需要抛出异常
    3. 创建目标对象
    4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
    5. 提交执行:Future<Boolean>result1=ser.submit(t1);
    6. 获取结果:boolean r1=result1.get()
    7. 关闭服务:ser.shutdownNow();
  • 下面为以commons-io工具包为基础的代码例子:

package ThreadStudy;

//创建线程的方式3:使用Callable接口,并重写call方法

import java.io.File;
import java.io.IOException;
import org.apache.commons.io.FileUtils;
import java.net.URL;
import java.util.concurrent.*;


//线程主体
public class ThreadStudy3 implements Callable<Boolean> {
    private String url;  //网络图片地址
    private String name; //保存的文件名
    public ThreadStudy3(String url,String name){
        this.url=url;
        this.name=name;
    }


    //下载图片线程的执行体
    @Override
    public Boolean call(){
        WebDownloader webDownloader=new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下载了文件名为:"+name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadStudy3 s1=new ThreadStudy3("https://i0.hdslb.com/bfs/album/86c2f074ad34b6df185ac6eb7dc3d2d1b7bde484.jpg@518w.webp","初音1.jpg");
        ThreadStudy3 s2=new ThreadStudy3("https://i0.hdslb.com/bfs/album/83cd3d5fbf54ef06f7bbf0bedba498cb0c7e2a0b.jpg@518w.webp","初音2.jpg");
        ThreadStudy3 s3=new ThreadStudy3("https://i0.hdslb.com/bfs/album/53d44d63b2f39a06cb2befbc0af220fba56af9b8.jpg@518w.webp","初音3.jpg");

        //创建执行服务:
        ExecutorService ser = Executors.newFixedThreadPool(3);

        //提交执行
        Future<Boolean> r1=ser.submit(s1);
        Future<Boolean> r2=ser.submit(s2);
        Future<Boolean> r3=ser.submit(s3);

        //获取结果
        boolean rs1=r1.get(); //此处抛出了异常
        boolean rs2=r2.get(); //此处抛出了异常
        boolean rs3=r3.get(); //此处抛出了异常
        System.out.println(rs1);
        System.out.println(rs2);
        System.out.println(rs3);
        
        //关闭服务
        ser.shutdownNow();
    }
}




//下载器
class WebDownloader{
    //下载方法
    public void downloader(String url,String name)  {
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            System.out.println("IO异常,downloader方法出现问题");
        }

    }
}

/*
输出结果为:
下载图片名为:初音1.jpg
下载图片名为:初音2.jpg
下载图片名为:初音3.jpg
true
true
true
并且根目录下会出现三个图片,有时三个图片的下载顺序会不同,因为线程是同时运行的
*/

commons-io工具包地址

表达式

  • Lambda表达式的用处:

    • 避免匿名内部类定义过多
    • 使代码变得简洁
    • 可以省去很多无意义代码,留下核心逻辑
  • 理解函数式接口(Functional Interface)对学习Lambda表达式至关重要。

  • 函数式接口的定义:

    • 任何接口,如果只包含唯一一个抽象方法,那么它被称为函数式接口。

      //example:
      @FunctionalInterface
      public interface Runnable{
          public abstract void run();
      }
      
    • 对于函数式接口,我们可以通过Lambda表达式来创建该接口的对象。

  • 代码例子

package LambdaStudy;

public class LambdaDemo01 {
    //表达一个程序有以下几种方式:

    //2.静态内部类
    static class ME2 implements Study{

        @Override
        public void lambda() {
            System.out.println("I study Lambda2");
        }
    }


    public static void main(String[] args) {
        //1.创建对象后引用方法
        Study me1= new ME();
        me1.lambda();


        //以下使静态内部类使用方法
        Study me2 =new ME2();
        me2.lambda();


        //3.局部内部类
        class ME3 implements Study{

            @Override
            public void lambda() {
                System.out.println("I study Lambda3");
            }
        }
        Study me3 =new ME3();
        me3.lambda();


        //4.匿名内部类
        Study me4=new Study() {
            @Override
            public void lambda() {
                System.out.println("I study Lambda4");
            }
        };
        me4.lambda();


        //5.Lambda表达式
        Study me5 =new ME();
        me5=()-> {
            System.out.println("I study Lambda5");
        };
        me5.lambda();

    }
}


//定义一个函数式接口
@FunctionalInterface
interface Study{
    void lambda();
}

//实现类
class ME implements Study{

    @Override
    public void lambda() {
        System.out.println("I study Lambda1");
    }
}
  • Lambda表达式简化代码例子:
package LambdaStudy;

public class LambdaDemo01 {
    public static void main(String[] args) {
        //1.标准的Lambda表达式
        Study study=(String a)->{
            System.out.println("I study Lambda"+"\t"+a);
        };
        study.lambda("标准");
        //2.简化参数(如果有两个及以上参数,则需要都简化,不能只简化一部分)
        study = (a)->{
            System.out.println("I study Lambda"+"\t"+a);
        };
        study.lambda("简化参数");
        //3.简化括号(如果参数为两个及以上,则要保留括号)
        study = a->{
            System.out.println("I study Lambda"+"\t"+a);
        };
        study.lambda("简化括号");
        //4.简化花括号:必须保证执行体只有一句指令代码
        study = a-> System.out.println("I study Lambda"+"\t"+a);
        study.lambda("简化花括号");
    }
}


//定义一个函数式接口
interface Study{
    void lambda(String a);
}

/*其输出结果为:
*I study Lambda	  标准
*I study Lambda	  简化参数
*I study Lambda	  简化括号
*I study Lambda	  简化花括号
*
* */

静态代理

  • 代理对象需要和直接对象实现同一个接口
  • 代理对象可以使用直接对象里面的方法
  • 其好处就是直接对象可以专注于实现自己的需要的方法,而代理对象可以一同实现许多直接对象的方法
package StaticProxy;

public class staticpro {
    public static void main(String[] args) {
        new BattleSystem(new Soilder()).Attacking();     //此处需要代理的对象为Soilder,通过battleSystem来间接实现士兵的攻击效果
        /*
        * 此处输出结果为:
        * 遇到Boss,开始战斗
        * 使用了物理攻击
        * 伤害不够,我们失败了
        * */

        //如果此处有两个角色来进行攻击
        new BattleSystem(new Soilder(),new Wizard()).Attacking();   //需要代理的对象变成了两个,分别是Soilder和Wizard,通过battleSysteman来间接实现两个角色的攻击效果
        /*
        * 此处输出结果为:
        *遇到Boss,开始战斗
        *士兵使用了物理攻击
        *魔法师使用了魔法攻击
        *战斗结束,我们胜利了
        * */
    }
}


interface Attack{     //攻击系统接口
    void Attacking();
}



class Soilder implements Attack{    //直接对象士兵实现接口
    @Override
    public void Attacking(){
        System.out.println("士兵使用了物理攻击");
    }
}

class Wizard implements Attack{    //直接对象魔法师实现接口

    @Override
    public void Attacking() {
        System.out.println("魔法师使用了魔法攻击");
    }
}

//代理角色,帮助自己
class BattleSystem implements Attack{   //代理对象战场系统实现接口

    private Attack target1;
    private Attack target2;

    public BattleSystem(Attack target){     //通过构造器将需要代理的对象传进去
        this.target1=target;
    }
    public BattleSystem(Attack target1,Attack target2){
        this.target1=target1;
        this.target2=target2;
    }
    @Override
    public void Attacking() {
        before();
        this.target1.Attacking();
        if(target2!=null) {
            this.target2.Attacking();
            after();
        }
        else{
            anotherafter();
        }
    }

    private void after() {
        System.out.println("战斗结束,我们胜利了");
    }

    private void before() {
        System.out.println("遇到Boss,开始战斗");
    }

    private void anotherafter(){
        System.out.println("伤害不够,我们失败了");
    }
}
  • 静态代理可以和Thread,Runnable接口进行对比,发现其原理

线程的状态

  • 创建状态
  • 就绪状态
  • 阻塞状态
  • 运行状态
  • 终止状态

线程具体流程

  1. 创建线程对象:例如Thread thread=new Thread();,线程会进入新建状态
  2. 创建完线程对象后调用start()方法时,线程会进入就绪状态,并不意味着线程被立即调度执行
  3. 当线程被cpu进行调度之后,线程就会进入运行状态,这个时候线程才会真正执行线程体的代码块
  4. 当线程对象调用sleep()wait()或同步锁时线程会进入阻塞状态,就是代码不往下执行,阻塞事件解除后,将会重新进入就绪状态,等待cpu的调度执行
  5. 如果线程没有进入阻塞状态,那么线程会在运行状态之后进入关闭状态,在线程中断或者结束的时候就会进入关闭状态,一旦进入则该线程就再也不能启动了

线程的方法

  • setPriority(int newPriority):更改线程的优先级
  • static void sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠
  • void join():使原有线程放弃执行,并调用该方法的返回对应线程,直至该线程执行完毕
  • static void yield():暂停当前正在执行的线程对象,使该线程处于就绪状态,重新等待cpu调度
  • void interrupt:中断线程
  • boolean isAlive():测试线程是否处于活动状态

停止线程(stop)

  • 不推荐使用JDK显示的已不推荐使用的方法,如stop(),destroy()等方法
  • 推荐令线程自己停下来
  • 可以通过使用一个标志位进行终止,令线程使用该标识的时候,通过对外提供方法来改变标识
  • 代码例子:
package ThreadStoptest;

public class StopTest implements Runnable{
    private boolean flag=true;
    @Override
    public void run() {
        int i=0;
        while (flag){
            System.out.println("get Hibiki "+i+" times");
            i++;
        }
    }

    public void stop(){
        this.flag=false;
    }

    public static void main(String[] args) {
        StopTest stopTest=new StopTest();
        new Thread(stopTest).start();
        for (int i = 0; i < 30; i++) {
            System.out.println("主线程的进行次数为"+i);
            if(i==25){
                stopTest.stop();
            }
        }
    }
}

线程的休眠(sleep)

  • 要让线程进行休眠需要用到sleep方法

    • sleep方法存在异常InterruptedException;
    • sleep时间达到后线程会重新进入就绪状态
    • sleep可以模拟网络延迟或者倒计时等
    • 每个对象都会有一个锁,且sleep不会释放锁
  • 代码例子(模拟倒计时):

package Demo01;

//模拟倒计时
public class TestSleep {
    public static void main(String[] args) {
        try {
            time();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static void time() throws InterruptedException {   //抛出InterruptedException异常
        int num = 13;
        while (true) {
            Thread.sleep(1000);   //设置延时为1000毫秒,1000毫秒等于1秒
            System.out.println(num--);
            if (num <= 0) {
                break;
            }
        }
    }
}

  • 获取系统时间进行倒计时:
package Demo01;

import java.text.SimpleDateFormat;
import java.util.Date;

//模拟倒计时
public class TestSleep {
    public static void main(String[] args) {
        Date startTime = new Date(System.currentTimeMillis());  //获取系统当前时间

        while (true) {
            try {
                Thread.sleep(1000); //延时1000毫秒
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime = new Date(System.currentTimeMillis());    //更新获取的系统时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

线程的让步(yield)

  • 线程的让步,让当前正在执行的线程暂停,但是不会阻塞
  • 将线程从运行状态转为就绪状态点击跳转状态具体流程
  • 需要让cpu重新调度,因此让步不一定成功
  • 举个比喻:一个盲人随机发射几门炮塔的炮弹,一次发射一个,一个炮弹引爆后才能随机发射其余的炮弹。yield就是让一个正在飞行的炮弹重新回到炮台,然后盲人去重新随机的发射一门炮塔的炮弹。
  • 代码例子:
package Demo01;

public class TestYield {
    public static void main(String[] args) {
        MyYield myYield=new MyYield();
        new Thread(myYield,"hibiki").start();
        new Thread(myYield,"ayanami").start();
        new Thread(myYield,"Raffe").start();
    }

}

class MyYield implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"线程开始执行"+i);
            Thread.yield();  //线程让行
System.out.println(Thread.currentThread().getName()+"线程停止执行"+i);
        }

    }
}

线程的强制加入(join)

  • join可以让原本执行的线程停止执行(进入阻塞状态)之后让调用该方法的线程优先执行,等待该线程执行完毕之后,原本的线程和其他线程才会继续执行。

  • 代码例子:

package JoinStudy;

public class TestJoin implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i < 100; i++) {
            System.out.println("塞壬插了"+i+"次队");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //创建线程对象
        TestJoin testJoin=new TestJoin();
        Thread thread =new Thread(testJoin);
        //启动线程
        thread.start();

        //主线程
        for (int i = 1; i < 1000; i++) {
            System.out.println("大家都在排队"+i);
            if(i%10==0) {
                thread.join();
            }
        }
    }
}

观测线程的状态

  • 基本流程

    • 需要先创建线程对象
    • 为变量设置类型Thread.state
    • 令变量等于线程调用getstate方法后获取的状态
    • 打印输出
  • 大致结构:

Thread thread =new Thread();
@Override
public void run(){
    xxxxxxxxxxxx;
    xxxxxxxxxxx;
}

Thread.State xxx = thread.getState();

System.out.println(xxx);
  • 代码例子:
package StateStudy;
import com.sun.media.sound.SoftTuning;
public class TestState {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("--------------");
        });

        //观测状态--------->NEW
        Thread.State state = thread.getState();
        System.out.println(state);

        //观测启动后的状态---------->Runnable
        thread.start();
        state=thread.getState();
        System.out.println(state);

        while(state!=Thread.State.TERMINATED){   //<-----只要线程不终止,就会一直运行下去
            thread.sleep(100);
            state=thread.getState();
            System.out.println(state);
        }
    }

}

线程的优先级

  • Java会提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器会按照优先级来调度哪个线程来执行。(最高优先级不一定被优先第一个调度,提高优先级的大小只能提高cpu调度其线程的概率)

  • 线程的优先级用数字表示,范围:1-10

    • Thread.MIN_PRIIRITY = 1;
    • Thread.MAX_PRIORITY= 10;
    • Thread.NORM_PRIORITY= 5;
  • 可以使用如下方式改变或获取优先级

    • getPriority().setPriority(int xxx);
  • 代码例子:

package PriorityStudy;

public class TestPriority {
    public static void main(String[] args) {
        //主线程默认优先级
        System.out.println(Thread.currentThread().getName()+"------->"+Thread.currentThread().getPriority());

        //其他线程
        MyPriority myPriority = new MyPriority();
        Thread m1=new Thread(myPriority);
        Thread m2=new Thread(myPriority);
        Thread m3=new Thread(myPriority);
        Thread m4=new Thread(myPriority);
        Thread m5=new Thread(myPriority);
        Thread m6=new Thread(myPriority);

        //先设置优先级,再启动
        m1.start();

        m2.setPriority(1);  //设置优先级1
        m2.start();

        m3.setPriority(4);
        m3.start();

        m4.setPriority(Thread.MAX_PRIORITY);
        m4.start();

        m5.setPriority(6);
        m5.start();

        m6.setPriority(5);
        m6.start();
    }
}

class MyPriority implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"------->"+Thread.currentThread().getPriority());
    }
}

守护线程

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

  • 虚拟机必须保证用户线程执行完毕

  • 虚拟机不用等待守护线程执行完毕

  • 代码例子:

package DaemonStudy;

public class TestDaemon {
    public static void main(String[] args) {
        Wizard wizard=new Wizard();
        Thread thread=new Thread(wizard);
        thread.setDaemon(true);    //设置为守护线程,如果是false则为用户线程
        Warrior warrior=new Warrior();
        thread.start();
        new Thread(warrior).start();
    }
}


//魔法师
class Wizard implements Runnable{

    @Override
    public void run() {
        while(true){   //判定为true,按理来说为无限循环
            System.out.println("魔法师为战士加了buff");
        }
    }
}

//战士
class Warrior implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("战士的能力加强了");
        }
        System.out.println("战士结束了战斗");
    }
}

线程的同步机制

基本概念

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

  • 多个线程访问同一个对象,并且某些线程还想修改这个对象,就需要线程同步

  • 线程同步其实是一种等待机制,多个需要同时访问此对象的线程会进入这个对象的等待池形成队列,等待前面的线程结束后再执行

  • 锁机制

    • 由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可
    • 存在以下问题:
      • 一个线程持有锁会导致其他所有需要此锁的线程挂起
      • 在多个线程竞争下,加锁和释放锁会导致比较多的上下文切换和调度延时,引起性能问题
      • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

同步方法及同步块

  • private关键字可用来保证数据对象只能被方法访问,而对于方法则提出了另一套机制,就叫做synchronized关键字

  • synchronized关键字包括两种用法:synchronized方法和synchronized块

    • 同步方法:public synchronized void method(int args){}
  • synchronized方法控制对"对象"的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就会独占该锁,直到该方法返回才释放锁,这样后面被阻塞的线程才能获得这个锁后再继续执行

  • synchronized关键字的缺点:将一个较大且复杂的方法声明为synchronized时会影响其效率

  • 同步方法代码例子:

package SynchronizedStudy;

public class TestSynchronized {
    public static void main(String[] args) {
        MeetCommander station = new MeetCommander();
        System.out.println("---------舰队回港后----------");
        new Thread(station,"Hibiki").start();
        new Thread(station,"Raffe").start();
        new Thread(station,"Z23").start();
    }
}

class MeetCommander implements Runnable{
    //见面次数
    private int meetNums = 10;
    boolean flag = true;   //外部写入停止方法
    @Override
    public void run() {
        //见面
        while(flag){
            try {
                meet();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    private synchronized void meet() throws InterruptedException {  //同步方法锁的是使用这个方法的对象,因此此处锁的是MeetCommander这个对象
        //判断还能见多少次
        if(meetNums<=0){
            flag=false;
            return;
        }
        //模拟延时
        Thread.sleep(100);
        //见面
        System.out.println(Thread.currentThread().getName()+"见了"+meetNums--+"次指挥官");
    }
}

倘若该代码不加上synchronized关键字,则该程序会出现负数的情况

  • 同步块:synchronized(Obj){} //一般锁的是需要改变其内部属性的对象,即会发生变化的对象
  • Obj称之为同步监视器
    • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是其本身的对象,或者是class
  • 同步监视器的执行过程
    1. 第一个线程访问,锁定同步监视器,执行其中代码
    2. 第二个线程访问,发现同步监视器被锁定,无法访问
    3. 第一个线程访问完毕,解锁同步监视器
    4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
  • 代码例子:
package SynchronizedStudy;
import java.util.List;
import java.util.ArrayList;


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

JUC安全类型

  • Java它本身会提供一个安全的ArrayList的类型,使用其类型也能达到synchronized关键字的效果
  • 代码例子:
package SynchronizedStudy;
import java.util.concurrent.CopyOnWriteArrayList;

public class TestSynchronized {
    public static void main(String[] args){
        CopyOnWriteArrayList<String> list= new CopyOnWriteArrayList<String>();
        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());
    }
}

死锁

  • 多个线程各自占有一些共享资源,当一个线程需要其他线程占有的资源才能运行时,会导致连锁反应使其他线程都在等待对方释放占有的资源,从而使全部线程都停止执行。当一个同步块同时拥有"两个以上对象的锁"时,就有可能发生"死锁"的问题。
  • 代码例子:
package LockStudy;

public class TestLock {
    public static void main(String[] args) {
        TakeFood g1=new TakeFood(1,"Hibiki");
        TakeFood g2=new TakeFood(0,"Ayanami");
        new Thread(g1).start();
        new Thread(g2).start();
    }
}

class Bread{

}

class Milk{

}

class TakeFood implements Runnable{
    //用static来保证资源只有一份
    static Bread bread =new Bread();
    static Milk milk =new Milk();

    int choice ;    //选择
    String Name;  //拿资源的人
    TakeFood(int choice,String Name){
        this.choice=choice;
        this.Name=Name;
    }
    @Override
    public void run() {
        try {
            takefood();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void takefood() throws InterruptedException {
        if(choice ==0){
            synchronized (bread){
                System.out.println(this.Name+"拿到了面包");
                Thread.sleep(1000);
                synchronized (milk){
                    System.out.println(this.Name+"拿到了牛奶");
                }
            }
        }
        else{
            synchronized (milk){
                System.out.println(this.Name+"拿到了牛奶");
                Thread.sleep(1000);
                synchronized (bread){
                    System.out.println(this.Name+"拿到了面包");
                }
            }
        }
    }
}
/*最后会发现程序执行到一一半之后就不会执行了,原因是线程同时执行的时候都需要对方所有的资源,从而导致双方都不能拿到自己需要的,因此程序相当于被打了个"死结"*/

  • 从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步(同步锁由Lock对象实现)。

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先得到Lock对象

  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,两者比较常用的是ReentrantLock,可以显式加锁和释放锁。

  • 基本用法:

class Name{
           private final ReentrantLock lock =new ReenTrantLock();
           public void m(){
           lock.lock();
           try{
                       //保证线程安全的代码
           }
               finally{
                   lock.unlock();
                   //如果同步代码有异常,要将unlock()写入finally语句块
               }
           }
           }
  • synchronized与Lock的对比
    • Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
    • Lock只有代码块锁,synchronized有代码块锁和方法锁
    • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的拓展性(提供更多的子类)
    • 优先使用顺序:
      • Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)

线程协作

生产消费者模式

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

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

    • wait():表示线程一直等待,直到其他线程通知,与sleep()不同,会释放锁
    • wait(long timeout):指定等待的毫秒数
    • notify():唤醒一个处于等待状态的线程
    • notifyAll():唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度
    • 注意:以上均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IIlegalMonitorStateException
  • 解决方式:

    • 解决方式1:

      • 并发协作模型"生产者/消费者模式"--->管程法
      • 生产者:负责生产数据的模块(可能是方法、对象、线程、进程);
      • 消费者:负责处理数据的模块(可能是方法、对象、线程、进程);
      • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个"缓冲区"

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

      Producer(生产者)--->数据缓存区---->Consumer(消费者)

    • 代码例子:

      package PCstudy;
      
      //测试生产者消费者模式--->管程法
      
      public class TestPC {
          public static void main(String[] args) {
              SynContainer container =new SynContainer();
      
              new Producer(container).start();
              new Consumer(container).start();
          }
      }
      
      //生产者
      class Producer extends Thread {
          SynContainer container;
      
          public Producer(SynContainer container) {
              this.container = container;
          }
          //生产
          @Override
          public void run(){
              for (int i = 0; i < 100; i++) {
                  System.out.println("生产了"+i+"个产品");
                  container.put(new Product(i));
              }
          }
      }
      //消费者
      class Consumer extends Thread{
          SynContainer container;
      
          public Consumer(SynContainer container){
              this.container=container;
          }
      
          //消费
          @Override
          public void run(){
              for (int i = 0; i < 100; i++) {
                  System.out.println("消费了--->"+container.pop().id+"个产品");
              }
      
          }
      }
      
      //产品
      class Product{
          int id;    //产品编号
      
          public Product(int id) {
              this.id = id;
          }
      }
      
      //缓冲区
      class SynContainer{
      
          //需要一个容器大小
          Product[] products= new Product[10];
      
          //容器计数器
          int count=0;
      
          //生产者放入产品
          public synchronized void put(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;
      
          }
      }
      
    • 解决方法2:

      • 并发协作模型"生产者/消费者模式"--->信号法
      • 例如给一个标志位进行一次判断,若为true则等待,false则唤醒其他线程
    • 代码例子:

    package PCStudy2;
    
    public class TestPC2 {
        public static void main(String[] args) {
            TVprogram tVprogram=new TVprogram();
            new Actor(tVprogram).start();
            new Audience(tVprogram).start();
        }
    }
    
    //观众
    class Audience extends Thread{
        TVprogram tVprogram;
        public Audience(TVprogram tVprogram) {
            this.tVprogram = tVprogram;
        }
        @Override
        public void run(){
            for (int i = 0; i < 20; i++) {
                tVprogram.watch();
            }
        }
    }
    
    //演绎人
    class Actor extends Thread{
        TVprogram tVprogram;
        public Actor(TVprogram tVprogram){
            this.tVprogram=tVprogram;
        }
        @Override
        public void run(){
            for (int i = 0; i < 20; i++) {
                if(i%2==0){this.tVprogram.play("扫黑风暴");}
                else{
                    this.tVprogram.play("bilibili");
                }
            }
        }
    }
    
    //电视剧
    class TVprogram {
        String program;   //表演的节目
        boolean flag=true;    //默认信标为T
    
        //演员表演 观众等待  T
        public synchronized void play(String program){
            if(!flag){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("演员表演了:"+program);
            this.notifyAll();   //通知观众观看
            this.program=program;
            this.flag=!this.flag;
        }
    
        //观众观看 演员表演   F
        public synchronized void watch(){
            if(flag){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("观看了:"+program);
            //通知演员表演
            this.notifyAll();
            this.flag=!this.flag;
        }
     }
    
    

线程池

  • 什么时候要使用线程池:经常创建和销毁、使用量特别大的资源。比如并发情况下的线程,这种情况会对性能影响很大。

  • 具体用法思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。

  • 好处:

    • 提高了响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
    • 便于线程的管理(不是方法):
      • corePoolSize:线程池大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止
  • JDK5.0之后提供了线程池相关的API:ExecutorServiceExecutors

    • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
      • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
      • <T>Future<T>submit(Callable<T>task):执行任务,有返回值,一般用来执行Callable
      • void shutdown():关闭连接池
    • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
posted @ 2021-09-25 15:02  __星海  阅读(121)  评论(0)    收藏  举报