潜入浅出,java多线程到底是个什么东东?面试中应该注意哪方面多线程的知识?

为了近期的面试,准备多线程的学习,这一部分十分重要,在我近期的面试中,问的十分多.尤其是创建线程三种方式,线程池的创建.

线程简介

主线程走主线程,子线程走子线程,main线程,gc线程(也可以称之为守护线程). 线程不能人为干预,可能会出现资源抢夺的问题,所以需要加上并发的控制.线程会带来额外的开销,比如cpu的调度时间

面试题:线程和进程的区别?

⼀个程序下⾄少有⼀个进程,⼀个进程下⾄少有⼀个线程,⼀个进程下也可以有多个线程来增加程序的执⾏
速度。举个例子,进程:包含视频,声音,弹幕,线程就是分别去执行视频,弹幕,线程。在这里可能会有并发跟并行的概念,并行,是多个cup同时工作,并发是一个cpu按照时间片的轮转,执行,尽管我们看着是同时进行的,但最终不是.

线程的创建

面试题:创建线程有哪⼏种⽅式?

创建线程有三种⽅式:
★继承 Thread 重写 run ⽅法;
★实现 Runnable 接⼝;
实现 Callable 接⼝。

一.继承 Thread 重写 run ⽅法;

因为java的单继承,所以不推荐使用.

入门实例

package com.xiucai;
// 继承Thread类,重写run方法,调用strat开启线程
public class TestThread extends Thread{

    @Override
    public void run() {
        //run方法线程体
        super.run();

        for (int i = 0; i < 20; i++) {
            System.out.println("run方法执行的此时"+i);
        }

    }
    //main线程,主线程
    public static void main(String[] args) {
    //1.创建线程对象
        TestThread testThread=new TestThread();
    //2.调用start()发放开启线程,使用start方法,可以让两个线程都执行
        testThread.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("主线程此时执行"+i);
        }
    }
}

这是调用start()方法,交替执行

image-20220126115926996

使用run方法,肯定是先执行run

image-20220126120053287

总结:线程开启不一定立即启动,需要等待cpu的调度

面试题 线程的 run()和 start()有什么区别?

start()⽅法⽤于启动线程,run()⽅法⽤于执⾏线程的运⾏时代码。run() 可以重复调⽤,⽽ start()只能调⽤
⼀次。

实例:多线程的图片下载

1.下载工具类:

链接:https://pan.baidu.com/s/1iDPhNoruFTpmHCxknAdcOQ 
提取码:g5mo 

image-20220126121207177

image-20220126121247238

image-20220126121252405

2.编写工具类

//下载器方法
class WebDownloader{
//   下载方法
public void donwloader(String url,String name){
    //这里的工具类是上一步执行的方法.传入第一个参数URL,再传入一个文件
    try {
        FileUtils.copyURLToFile(new URL(url),new File(name));
    } catch (IOException e) {
        e.printStackTrace();
        System.out.println("IO异常.downloader出现异常");
    }
}

}

3,编写构造函数传入数据

public class TestThread2 extends Thread{
   private String url;
   private String name;
   //构造函数
   public TestThread2(String url,String name){
  this.url=url;
  this.name=name;
   }




   //多线程函数体
    @Override
    public void run() {
        super.run();
        WebDownloader webDownloader=new WebDownloader();
        webDownloader.donwloader(url,name);
        System.out.println("下载的文件名"+name);

    }

    //主函数
    public static void main(String[] args) {
        //创建线程对象

     TestThread2 testThread2   =new TestThread2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fbj-yuantu.fotomore.com%2Fcreative%2Fvcg%2Fnew%2FVCG211356080686.jpg%3FExpires%3D1643621486%26OSSAccessKeyId%3DLTAI2pb9T0vkLPEC%26Signature%3DtLJzWT8OEpGQ4MHADL%252Fhz9SebeY%253D%26x-oss-process%3Dimage%252Fauto-orient%252C0%252Fsaveexif%252C1%252Fresize%252Cm_lfit%252Ch_1200%252Cw_1200%252Climit_1%252Fsharpen%252C100%252Fquality%252CQ_80%252Fwatermark%252Cg_se%252Cx_0%252Cy_0%252Cimage_d2F0ZXIvdmNnLXdhdGVyLTIwMDAucG5nP3gtb3NzLXByb2Nlc3M9aW1hZ2UvcmVzaXplLG1fbGZpdCxoXzE3MSx3XzE3MSxsaW1pdF8x%252F&refer=http%3A%2F%2Fbj-yuantu.fotomore.com&app=2002&size=f10000,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1645764262&t=e5f59f659774002781376801cd533c5b","2.jpg");
        testThread2.start();
    }

}

image-20220126130658528

二.实现接口Runable,推荐使用

灵活方便

package com.xiucai;

/**
 * 实现runable接口,重写run方法,执行线程,需要丢入runable接口的实现类
 */
public class TestThread3 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("run方法执行的此时"+i);
        }
    }

    public static void main(String[] args) {
       //创建runnable接口的实现类对象
        TestThread3 testThread3=new TestThread3();
        //创建线程对象,通过线程对象来开启线程
        Thread thread=new Thread(testThread3);
        thread.start();

        //new Thread(testThread3).start();

        for (int i = 0; i < 20; i++) {
            System.out.println("主线程此时执行"+i);
        }
    }
}

实现上边run的方法那边,这里值得深究

package com.xiucai;

/**
 * 实现runable接口,重写run方法,执行线程,需要丢入runable接口的实现类
 */
public class TestThread3 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("run方法执行的此时"+i);
        }
    }

    public static void main(String[] args) {
       //创建runnable接口的实现类对象
        TestThread3 testThread3=new TestThread3();
        //创建线程对象,通过线程对象来开启线程
        Thread thread=new Thread(testThread3);
        thread.start();

        //new Thread(testThread3).start();

        for (int i = 0; i < 20; i++) {
            System.out.println("主线程此时执行"+i);
        }
    }
}

实例:龟兔赛跑

package com.xiucai;
//1.创建多线程类
public class TestThread6 implements Runnable{
    //胜利者
    private static String winner;
    @Override
    //2.重写多线程run方法
    public void run() {
//        for是赛道,i是米数
        for (int i = 1; i <= 100; i++) {
            //模拟兔子休息,十步一休息.
            if (Thread.currentThread().getName().equals("兔子") && i%10==0){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //判断比赛是否结束,这是一个判断终点的方法.
            boolean flag=gameOver(i);
           //把返回值取回来.
            if (flag){
                break;
            }
            
            System.out.println(Thread.currentThread().getName()+":跑了"+i+"步");
        }
    }
    //3.判断是否完成比赛
    private boolean gameOver(int steps){
   if (winner!=null){
       //比赛结束
       return true;
   }
  if (steps>=100){
      winner=Thread.currentThread().getName();
      System.out.println("胜利者为:"+winner);
      return true;
  }
  return false;
    }
 //4.编写运行实例
    public static void main(String[] args) {
        //testThread6极为赛道
        TestThread6 testThread6=new TestThread6();
        new Thread(testThread6,"兔子").start();
        new Thread(testThread6,"乌龟").start();
    }
}

整个逻辑都是的实现都是在run方法里边的,用i作为资源,兔子和乌龟都去争夺.

三、线程不安全实例-买票

package com.xiucai;

/**
 * 多个线程操作一个对象
 * 假设买火车票
 */
public class TestThread5  implements Runnable {
      private int ticketNum = 10;



    @Override
    public void run() {
        while (true) {
            //Thread.currentThread().getName() 得到名字

            if (ticketNum <= 0) {
           break;
                   }
            //模拟延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //输出拿到了多少张票
            System.out.println(Thread.currentThread().getName() + ":拿到了,第几张票" + ticketNum--);
        }



    }

    public static void main(String[] args) {
     //小牛,小蓝,小紫
        TestThread5 ticket   =new TestThread5();
        //上边的 Thread.currentThread().getName() 得到名字
        new Thread(ticket,"小明").start();
        new Thread(ticket,"小牛").start();
        new Thread(ticket,"小紫").start();
    }
}

以上程序实现了以下效果,我们发现,在进行相同资源的时候.不好使了.也可以称之为线程不安全

小紫:拿到了,第几张票10
小明:拿到了,第几张票9
小牛:拿到了,第几张票8
小紫:拿到了,第几张票7
小明:拿到了,第几张票5
小牛:拿到了,第几张票6
小紫:拿到了,第几张票4
小牛:拿到了,第几张票3
小明:拿到了,第几张票2
小明:拿到了,第几张票1
小牛:拿到了,第几张票0
小紫:拿到了,第几张票1

四、callable接口

实现Callable接口,需要返回值类型,可以抛出异常

package com.xiucai;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

/**
 * 线程创建方式3,实现callable接口.
 */
public class TestThread7 implements Callable {
    private String url;
    private String name;
    //构造函数
    public TestThread7(String url,String name){
        this.url=url;
        this.name=name;
    }




    //多线程函数体
    @Override
    public Object call() throws Exception {
        WebDownloader webDownloader=new WebDownloader();
        webDownloader.donwloader(url,name);
        System.out.println("下载的文件名"+name);
        return true;
    }

    //主函数
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建线程对象

        TestThread7 testThread7   =new TestThread7("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fbj-yuantu.fotomore.com%2Fcreative%2Fvcg%2Fnew%2FVCG211356080686.jpg%3FExpires%3D1643621486%26OSSAccessKeyId%3DLTAI2pb9T0vkLPEC%26Signature%3DtLJzWT8OEpGQ4MHADL%252Fhz9SebeY%253D%26x-oss-process%3Dimage%252Fauto-orient%252C0%252Fsaveexif%252C1%252Fresize%252Cm_lfit%252Ch_1200%252Cw_1200%252Climit_1%252Fsharpen%252C100%252Fquality%252CQ_80%252Fwatermark%252Cg_se%252Cx_0%252Cy_0%252Cimage_d2F0ZXIvdmNnLXdhdGVyLTIwMDAucG5nP3gtb3NzLXByb2Nlc3M9aW1hZ2UvcmVzaXplLG1fbGZpdCxoXzE3MSx3XzE3MSxsaW1pdF8x%252F&refer=http%3A%2F%2Fbj-yuantu.fotomore.com&app=2002&size=f10000,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1645764262&t=e5f59f659774002781376801cd533c5b","2.jpg");
                //创建执行服务,与线程池有关系
        ExecutorService ser= Executors.newFixedThreadPool(1);
        //提交执行
        Future<Boolean> r1=ser.submit(testThread7);
//    获取结果
        boolean rs1=r1.get();
        System.out.println(rs1);
        //关闭服务
        ser.shutdownNow();
    }


}

底层原理

一.静态代理

找个房产代理,婚庆公司,都是代理.

image-20220126180113393

package com.xiucai;

/**
 * 静态代理类的实现
 * 真是对象和代理对象都要实现一个接口,都实现Marry.
 * 代理对象,要把代理对象传入.
 * 很形象的显示,真实对象专注做自己的事情
 */
public class StaticProxy {
    public static void main(String[] args) {
       //创建我
        Marry marryMe =new MarryMe();
        //先搞出一个代理公司来,把我这个需要代理的传入
        Company company=new Company(marryMe);
       //调用的是婚庆公司的的结婚方法.
        company.HappyMarry();


    }

}
interface Marry{
    void HappyMarry();
}
//真实角色,我结婚
class MarryMe implements Marry {

    @Override
    public void HappyMarry() {
        System.out.println("我要结婚了");
    }
}
//公司代理
class  Company implements  Marry{
    //目标对象要结婚
  private Marry target;
  //构造函数,把代理对象传入.
  public  Company(Marry target){
      this.target=target;
  }

    @Override
    public void HappyMarry() {
      before();
      //调用目标对象的方法
      this.target.HappyMarry();
      after();

    }



    private void before() {
        System.out.println("结婚之前订车");
    }
    private void after() {
        System.out.println("结婚打扫卫生");
    }
}

二、Lamda 表达式,

为了简化代码,实现

函数式接口.任何接口只有一个抽象方法.

//1.定义一个函数式接口.
interface Ilike{
void lamda();
}

2.常用的方式,

//2.实现类
class Like implements Ilike{

    @Override
    public void lamda() {
        System.out.println("i like lamda");
    }
}

结果(正常输出):image-20220126185141181

3,进一步简化,写在测试类里边

//3.静态内部类,把实现类放在静态内部类中.

 static  class Like2 implements Ilike{

    @Override
    public void lamda() {
        System.out.println("i like lamda你静态内部类");
    }
}

4,局部内部类,写在main函数里边

//4.局部内部类,放在了方法里边了
class Like3 implements Ilike{

    @Override
    public void lamda() {
        System.out.println("i like lamda局部内部类");
    }
}
//在进行局部内部类时,我把代码放在上边不好用,只能放在下边.

like= new Like3();
like.lamda();

5,我们没有显性的去定义一个类没有类的名称必须借助接口或者父类,在main中

//5.匿名内部类,没有类的名称必须借助接口或者父类
like = new Ilike() {
     @Override
     public void lamda() {
         System.out.println("这是匿名内部类");
     }
 };
//匿名内部类的测试.
like.lamda();

6.用lamda简化,我们实现了函数式接口 注意 Ilike like= new Like(); 在main中

like=()->{
    System.out.println("用lamda简化");
};
like.lamda();

以上的方法,可以避免写太多的匿名内部类

另一个实例

package com.xiucai;

public class TestLamda {
//
public static void main(String[] args) {
    Student student=(int i)->{
        System.out.println("lamada表达式");
    };
    student.study(1);
    //这就相当于
    student=new Student() {
        @Override
        public void study(int a) {
            System.out.println("这是匿名内部类");
        }
    };
    student.study(1);

}
}
interface Student{
    //函数式接口.任何接口只有一个抽象方法.
    void study(int a);
}

lamada表达式代码简化

  Student student=(int i)->{
        System.out.println("lamada表达式");
    };

//  Student student=(int i)->{ 参数类型可以去掉,如果多个参数的时候,一去都去
//    Student student=( i)->{  一个参数的时候括号可以简化
//    Student student= i->{    当后边只有一行代码的时候花括号可以简化
    Student student= i->{
        System.out.println("lamada表达式");
    };
    student.study(1);

由于runable接口只有一个方法,所以可以叫函数式接口,进而可以使用lamada表达式

image-20220126190929789

三、线程状态

线程的五种状态

image-20220126191903968

观察线程状态

package com.xiucai;

public class TestThreadState {

    public static void main(String[] args) throws InterruptedException {
        //此时已经把thread类 runable接口实现的new 丢进去了
    Thread thread=new Thread(()->{
        for (int i = 0; i < 5; i++) {
            System.out.println("这里里边是线程的执行方法"); //执行
            try {
                Thread.sleep(100);//主动阻塞
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }



    });

        System.out.println(thread.getState()); //输出new 创建
      thread.start();
        System.out.println(thread.getState());//输出 RUNNABLE 就绪
 while(thread.getState()!=Thread.State.TERMINATED){
      Thread.sleep(100);
     System.out.println(thread.getState());//TIMED_WAITING  线程内部主动阻塞
 }
        System.out.println(thread.getState()); //输出 TERMINATED

    }




}

线程停止

package com.xiucai;

public class ThreadStop 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) {
        ThreadStop threadStop=new ThreadStop();
         new Thread(threadStop).start();
        //主线程执行,分线程也在执行.
         for (int i = 0; i < 100; i++) {
            System.out.println("main线程执行了多少次了"+i);
            if(i==90){
                //线程的停止.操作这个线程对象.
                threadStop.stop();
                System.out.println("线程该停止了");
            }

        }
    }
}

结果:image-20220126195855434

线程延时

:为了放大问题的发生性

每个对象都会有一把锁,sleep不会释放锁

package com.xiucai;

public class ThreadSleep implements Runnable{


    private int ticketNum = 10;



    @Override
    public void run() {
        while (true) {
            //Thread.currentThread().getName() 得到名字

            if (ticketNum <= 0) {
                break;
            }
            //模拟延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //输出拿到了多少张票
            System.out.println(Thread.currentThread().getName() + ":拿到了,第几张票" + ticketNum--);
        }



    }

    public static void main(String[] args) {
        //小牛,小蓝,小紫
        ThreadSleep ticket   =new ThreadSleep();
        //上边的 Thread.currentThread().getName() 得到名字
        new Thread(ticket,"小明").start();
        new Thread(ticket,"小牛").start();
        new Thread(ticket,"小紫").start();
    }


}

模拟倒计时

public static void main(String[] args) throws InterruptedException {
    ThreadSleep.freetime();




}
public  static  void freetime() throws InterruptedException {

    int num=10;
    while(true){
        Thread.sleep(1000);
        System.out.println(num--);
        if (num<=0){
            break;
        }
    }
}

打印当前时间

我再这里尝试了,不先创建一个时间对象,然后结果不太好.

//获取当前系统时间
public  static  void currTime() throws InterruptedException {

    //创建一个时间对象,显示当前的一个时间
    Date startTime=new Date(System.currentTimeMillis());
    //一直在循环输出一个时间
    while(true){
        Thread.sleep(1000);
        //格式化当前对象,这个应该是
        System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
        startTime=new Date(System.currentTimeMillis());
    }
 public static void main(String[] args) throws InterruptedException {


ThreadSleep.currTime();


 }

线程礼让 yield()

礼让不一定成功,礼让只是让cpu中的东西出来,看cpu的心情,下图例子跟代码

image-20220126211215609

package com.xiucai;

public class TestYield {
    public static void main(String[] args) {
       MyYield myYield =new MyYield();
       new Thread(myYield,"a").start();
       new Thread(myYield,"b").start();
    }
}
class MyYield implements Runnable{


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

        }


}

合并线程 Join

代此线程执行完成,其余的线程才可以执行.

如下分为子线程,以run操作,跟主线程,我采用子线程.join的形式进行完成了.

package com.xiucai;

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

public class TestJosn implements Runnable{
    public static void main(String[] args) throws InterruptedException {
        MyYield myYield =new MyYield();
       Thread thread = new Thread(myYield,"a");
        thread.start();
        for (int i = 0; i < 10; i++) {
            thread.join();
            System.out.println("主线程"+i++);

        }

    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程开始执行");


        }
    }
}

线程的优先级

package com.xiucai;

public class ThreadPriority {




        public static void main(String[] args) throws InterruptedException {
            //此时已经把thread类 runable接口实现的new 丢进去了,我用了Lam大表达式
            Thread thread=new Thread(()->{

                System.out.println("这是线程"+Thread.currentThread().getPriority());
            });
            Thread thread2=new Thread(()->{

                System.out.println("这是线程二"+Thread.currentThread().getPriority());
            });


            thread2.setPriority(2);
            thread.setPriority(1);
            thread.start();
            thread2.start();
            for (int i = 0; i < 10; i++) {
                System.out.println("这是主线程"+Thread.currentThread().getPriority());
            }



        }




    }

输出,

image-20220129112943935

守护线程 daemon

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

jvm必须确保用户线程执行完成 (main),虚拟机不用等待守护线程执行完毕(gc)

package com.xiucai;

public class ThreadDaemon {
    public static void main(String[] args) {
        Deamon deamon = new Deamon();
        Son son = new Son();
        Thread thread = new Thread(deamon);
        Thread thread_son = new Thread(son);
        thread.setDaemon(true);//true 表示是守护线程.false是用户线程
        thread.start();
        thread_son.start();
    }

}
class Deamon implements Runnable{

    @Override
    public void run() {
      while (true){
          System.out.println("守护线程在这执行");
      }
    }
}
class Son implements Runnable{

    @Override
    public void run() {
        int i=0;
        while (i<100){
            System.out.println("用户线程在这执行");
            i++;
        }
    }
}

image-20220129115420773

四、线程同步

并发:多个线程操作一个对象.上边的买票例子.

线程同步是一种等待机制

队列加锁才能保证线程的安全性,每个线程都有一把锁

银行不安全实例
package syn;

import java.math.BigDecimal;

/**
 * 不安全的银行取钱
 */
public class Unsafeback implements Runnable{

    BigDecimal tallMoney =new BigDecimal("1000");
    /** 取了多少钱*/
    BigDecimal getmoney =new BigDecimal("500");


   //取钱在这里里边写
    @Override
    public void run() {
     //执行取钱的指令
        getMonry();
        try {
            Thread.sleep(1000);//在这里加上sleep没用 输出的都是 0,0
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    /**取钱的方法*/
    public  boolean getMonry(){
        // -1小于,0相等,1大于 没钱了取钱失败
        if (tallMoney.compareTo(BigDecimal.ZERO)==-1){
        return  false;
        }
        //有钱输出取完了的钱剩下多少钱
        tallMoney= tallMoney.subtract(getmoney);
        System.out.println(Thread.currentThread().getName()+"剩下多少钱"+tallMoney);
         return true;
    }
  //谁去取钱
    public static void main(String[] args) throws InterruptedException {
        Unsafeback account=new Unsafeback();
       Thread me= new Thread(account,"我");
       Thread wife= new Thread(account,"对象");
         me.start();
        Thread.sleep(1000);//在这里加上sleep就变成类似于单线程了
        wife.start();
    }
}

List不安全实例

经过测试list跟ArrayList都是线程不安全的.Hashtabale以及Vector是线程安全的,这也是面试中常考的

package syn;

import java.util.ArrayList;
import java.util.List;

/**
List数组是线程不安全的实例
 */
public class ThreadListy {
    public static void main(String[] args) {
        List<String> lists = new ArrayList<>();
        //启动1000个线程,往数组里边放对象
        for (int i = 0; i < 1000; i++) {
             // 采用lanmada表达式
            Thread thread=new Thread(()->{
                lists.add(Thread.currentThread().getName());

            });
            thread.start();
        }

       //查看到底放进去了多少个对象
        System.out.println("数组大小"+ lists.size());

    }
}

执行结果预期是1000,实际上是977,这说明有一部分位置被覆盖掉了

image-20220129185454567

同步方法与同步块 synchronized

牢牢把握,synchronized解决的是变化的量,是需要增删改查的对象。

在解决单对象问题的时候,比如买票问题,买票问题本身操作的就是票的数量

image-20220129201017132

这里针对于上边的代码的银行不安全实例,狂神的实例中,写了account对象作为银行账户,但是对于我上边的代码,可以明显的,基本上与买票问题很相似.所以说我还是在本身买票的方法上加了, 关键字实现了效果.

image-20220129201401032

最后通过List不安全的例子来看优化方法,首先确定我们操作的就是List数组对象,具体来说是lists对象.那就用synchronized 代码块来实现本实例

出现了问题,在for循环启动完线程之后,实际上还有一些线程没有执行完成,在一直往里边存数,尽管加了锁,但是与主线程里边的输出数组大小这个程序冲突,所以对于主线程看数组大小的命令,延时一段时间之后会变好.

JUC 并发 里边的安全类型 CopyOnWriteArrayList

读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全,而且也是线程安全的。

CopyOnWriteArrayList 写入操作 add() 方法在添加集合的时候加了锁,保证同步,避免多线程写的时候会 copy 出多个副本。

package com.xiucai;
/**
 * 线程实例-JUC并发编程
 */

import java.util.concurrent.CopyOnWriteArrayList;

public class TestJUC {


    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList copyOnWriteArrayList=new CopyOnWriteArrayList();
        for (int i = 0; i < 1000; i++) {
      new Thread(()->{
          copyOnWriteArrayList.add(Thread.currentThread().getName());

      }).start();
        }
        Thread.sleep(100);
        System.out.println(copyOnWriteArrayList.size());

    }
}

执行结果

image-20220130103026284

死锁

相互需要对方的资源,但这两个线程都在执行,谁都不愿意给谁,形成僵持

产生死锁的四个必要条件

  • 互斥条件:一个资源只能一个进程使用,我用口红,你也想用口红
  • 请求与保持的条件:我想过去拿你的口红,镜子我还不想给你
  • 不剥夺条件:进程获得的资源,未使用完成之前,不能强行剥夺
  • 循环等待条件:若干进程行成一种循环等待资源关系
  1. 出现死锁,条件,互斥条件

解释一下,两个资源 镜子跟口红,小美用着口红,小红用着镜子.在锁内写了一个锁,造成了手里抱着一个锁,还想再去搞个锁.

写了两个类代替口红跟镜子,写了化妆的方法,来具体实现锁,然后启动了两个线程

package syn;

public class DeadLock implements Runnable{
     static Rouge rouge = new Rouge();
    static Mirror mirror = new Mirror();

    /**
     * 执行线程
     */
    @Override
    public void run() {
        makeup();
    }
    //化妆方法执行
    private void makeup(){
    if (Thread.currentThread().getName().equals("小美")){
        synchronized (rouge){
            System.out.println(Thread.currentThread().getName()+"在用口红想要镜子");
            //锁内写锁
            synchronized (mirror){
                System.out.println(Thread.currentThread().getName()+"我还想要镜子");
            }
      }
      //小红想怎么搞
    }else{
        synchronized (mirror){
            System.out.println(Thread.currentThread().getName()+"在用镜子想要口红");
            //锁内写锁
            synchronized (rouge){
                System.out.println(Thread.currentThread().getName()+"我还想要口红");
            }
        }
    }

    }

    public static void main(String[] args) {
        DeadLock deadLock=new DeadLock();

        new Thread(deadLock,"小美").start();
        new Thread(deadLock,"小红").start();
    }
}
/** 资源一:口红*/
class Rouge{

}
/**
 资源二:镜子
 */
class Mirror{

}
  1. 解决死锁,锁外加锁
package syn;

public class DeadLock implements Runnable {
       static Rouge rouge = new Rouge();
    static Mirror mirror = new Mirror();


    /**
     * 执行线程
     */
    @Override
    public void run() {
        makeup();
    }

    //化妆方法执行
    private void makeup() {
        if (Thread.currentThread().getName().equals("小美")) {
            synchronized (rouge) {
                System.out.println(Thread.currentThread().getName() + "在用口红想要镜子");
                //锁内写锁
//            synchronized (mirror){
//                System.out.println(Thread.currentThread().getName()+"我还想要镜子");
            }
            //变成锁外写锁
            synchronized (mirror) {
                System.out.println(Thread.currentThread().getName() + "我还想要镜子");
            }
            //小红想怎么搞
        } else {
            synchronized (mirror) {
                System.out.println(Thread.currentThread().getName() + "在用镜子想要口红");
                //锁内写锁
//            synchronized (rouge){
//                System.out.println(Thread.currentThread().getName()+"我还想要口红");
//            }
            }
            //变成锁外写锁
            synchronized (rouge) {
                System.out.println(Thread.currentThread().getName() + "我还想要口红");
            }

        }
    }

    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();

        new Thread(deadLock, "小美").start();
        new Thread(deadLock, "小红").start();
    }
}

    /**
     * 资源一:口红
     */
    class Rouge {

    }

    /**
     * 资源二:镜子
     */
    class Mirror {

    }

以上的例子正好跟上边的银行买票不安全实例对应一下代码块跟代码关键字的区别.

面试题:多线程中 synchronized 锁升级的原理是什么

synchronized 锁升级原理:在锁对象的对象头⾥⾯有⼀个 threadid 字段,在第⼀次访问的时候 threadid
为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进⼊的时候会先判断 threadid 是否与其
线程 id ⼀致,如果⼀致则可以直接使⽤此对象,如果不⼀致,则升级偏向锁为轻量级锁,通过⾃旋循环⼀
定次数来获取锁,执⾏⼀定次数之后,如果还没有正常获取到要使⽤的对象,此时就会把锁从轻量级升级为
重量级锁,此过程就构成了 synchronized 锁的升级。

面试题:怎么防⽌死锁?

尽量使⽤ tryLock(long timeout, TimeUnit unit)的⽅法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防⽌死锁。
尽量使⽤ java.util.concurrent 并发类代替⾃⼰⼿写锁,在上边JUC并发类,就能看到。
尽量降低锁的使⽤粒度,尽量不要⼏个功能⽤同⼀把锁。
尽量减少同步的代码块。

面试题: synchronized 和 volatile 的区别是什么?

volatile 是变量修饰符;synchronized 是修饰类、⽅法、代码段。
volatile 仅能实现变量的修改可⻅性,不能保证原⼦性;⽽ synchronized 则可以保证变量的修改可⻅
性和原⼦性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

面试题 static的作用

static修饰的变量,在类加载时会被分配到数据区的方法区。

static修饰的方法中不能使用this或super,static修饰的方法属于类的方法,而this或super只是对象的方法。

面试: synchronized和volatile的区别?

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性

LOCK锁

出现了一个问题,我在复制测试lock锁的时候,把image-20220130134202097忘记更改,直接用了这个类实现了Runnable接口,实际上应该写成

   TestLock buyTicket = new TestLock();

也进一步说明了,线程对象会去调用,线程对象本身的方法。

package com.xiucai;


import java.util.concurrent.locks.ReentrantLock;

/**
 * 测试unlock 与lock
 */
public class TestLock implements Runnable {
    //票数,注意这个可以是参数通过注入更改可以传一下
    private int tickets=10;
    /**1.实现ReentrantLock对象 */
   ReentrantLock lock= new ReentrantLock();
    //线程执行内容,即循环买票
    @Override
    public void run() {
        boolean buy;
        while(true) {
            try {
                //2.这里加锁,锁增删改查的对象
               lock.lock();
                buy = buyTicket2();
                if (buy==false){
                    break;
                }

                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();

            }finally {
                //3.这里解锁
            lock.unlock();
            }


        }



    }

    //买票的方法
    public     boolean buyTicket2(){
        //如果没票,返回失败
        while(tickets<=0){
            return false;
        }
        //lock.lock(); 这里
        System.out.println(Thread.currentThread().getName()+"买票成功"+tickets--);
        //lock.unlock();
        return true;
    }

    public static void main(String[] args) {
        TestLock buyTicket = new TestLock();
        new Thread(buyTicket,"A").start();
        new Thread(buyTicket,"B").start();
        new Thread(buyTicket,"C").start();


    }
}

五、线程协作

生产者消费者模式(管程法:利用缓冲区解决问题)

Object类下边的几个需要用的方法

  • wait() :表示线程等待,直到其他线程通知,它不会释放锁,sleep抱着锁
  • wait():指定等待的毫秒数
  • notify():唤醒一个处于等待的线程
  • notifyAll:欢迎一个对象上所有调用wait方法的线程

image-20220130205904351

package Cooperate;

import java.util.function.Consumer;

/**
 * 这是一个测试多线程的类
 */
public class TestCooperate {
    public static void main(String[] args) {
        Container container=new Container();
        new Producutor(container).start();
        new Customer(container).start();
    }



}
/**生产者,只需要管生产即可*/
class Producutor extends Thread{
    Container container=new Container();
    Producutor(Container container){
        this.container=container;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                container.push(new Chicken());
                System.out.println("生产的第"+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }
}
/*
消费者
 */
class Customer extends Thread{
    Container container=new Container();
    Customer(Container container){
        this.container=container;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                container.pop(new Chicken());
                System.out.println("消费的第"+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }

}
//模拟产品,鸡
class Chicken{ }



//中间的容器,看成一个对象,欢迎一个对象上所有调用wait方法的线程
class Container {

// 需要一个容器大小
    Chicken[] chickens=new Chicken[10];
//容器计数器
    int count=0;
 //生产者放入产品
    public synchronized void push(Chicken chicken) throws InterruptedException {
       //如果容器不满的情况下可以放入产品
        if (count== chickens.length){
            //如果不能放,当前线程给休息起来
            this.wait();
        }
        chickens[count++]=chicken;
        //可以通知消费者了
        this.notifyAll();
    }
   // 消费者拿出商品
   public synchronized Chicken pop(Chicken chicken) throws InterruptedException {
       //如果容器不空的情况下可以消费产品
       if (count==0){

           //此时让消费者等待,生产者起
           this.wait();
           return chicken;


       }
       chicken= chickens[--count];
       //吃完了通知消费者
       this.notifyAll();
       return chicken;

   }

}
生产者消费者模式(设置标志位的解决方式)

消费者和生产者是两个线程

package Cooperate2;



/**
 * 将利用缓冲区解决问题,变成设立标志位解决问题
 */
public class TestCooperate {
    public static void main(String[] args) {
        Container container=new Container();
        new Producutor(container).start();
        new Customer(container).start();
    }



}

/**生产者,只需要管生产即可*/
class Producutor extends Thread{
    Container container=new Container();
    Producutor(Container container){
        this.container=container;
    }
    @Override
    public void run() {
        //在线程里一共生产是10只往里边放
        for (int i = 0; i < 10; i++) {

            try {

                container.push();
                System.out.println("这是在往里边放鸡,放的第"+i+"只鸡");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


    }


        }



/*
消费者
 */
class Customer extends Thread{
    Container container=new Container();
    Customer(Container container){
        this.container=container;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {

            try {

                container.pop();
        
                System.out.println("这是在往里边吃鸡,吃的第"+i+"只鸡");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}

//模拟产品,鸡
class Chicken{ }


//中间的容器,看成一个对象,欢迎一个对象上所有调用wait方法的线程
class Container {

       //设置标志位,当T为生产,F为消费
    boolean flag=true;
 //生产者放入产品
    public synchronized void push() throws InterruptedException {
       //标志位为F的时候为消费,此时不能生产,所以用wait让其等等
        if (flag==false){
            //如果不能放,当前线程给休息起来
            this.wait();
        }
       //如果为ture
        //可以通知消费者了

        this.notifyAll();
        flag=!flag;
    }
   // 消费者拿出商品
   public synchronized void pop() throws InterruptedException {
       //如果容器不空的情况下可以消费产品
       if (flag==true){

           //此时让消费者等待,生产者起
           this.wait();

       }

       //吃完了通知生产者
       this.notifyAll();
       flag=!flag;

   }

}

面试题:sleep() 和 wait() 有什么区别?

类的不同:sleep() 来⾃ Thread,wait() 来⾃ Object

释放锁:sleep() 不释放锁;wait() 释放锁

⽤法不同:sleep() 时间到会⾃动恢复;wait() 可以使⽤ notify()/notifyAll()直接唤醒。

线程池

可以避免创建销毁,实现重复利用,便于线程销毁跟创建

线程池

package Cooperate;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {


    public static void main(String[] args) {
        //1.创建服务,创建线程池 ,参数为线程池的大小
        ExecutorService service= Executors.newFixedThreadPool(10);
        //2.启动线程
        service.execute(new MyThread() );
        service.execute(new MyThread() );
        service.execute(new MyThread() );
        service.execute(new MyThread() );
        //3.关闭连接
        service.shutdown();
    }
}
class  MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println("线程:"+Thread.currentThread().getName());

    }

}

image-20220131182254452

与callable之间的对比

image-20220131182426349

面试题 说⼀下 runnable 和 callable 有什么区别?

runnable 没有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的补充。

面试题:创建Thread得几种方式

  1. newSingleThreadExecutor():它的特点在于⼯作线程数⽬被限制为 1,操作⼀个⽆界的⼯作队列,所
    以它保证了所有任务的都是被顺序执⾏,最多会有⼀个任务处于活动状态,并且不允许使⽤者改动线
    程池实例,因此可以避免其改变线程数⽬;
  2. newCachedThreadPool():它是⼀种⽤来处理⼤量短时间⼯作任务的线程池,具有⼏个鲜明特点:它
    会试图缓存线程并重⽤,当⽆缓存线程可⽤时,就会创建新的⼯作线程;如果线程闲置的时间超过 60 秒,则被终⽌并移出缓存;⻓时间闲置时,这种线程池,不会消耗什么资源。其内部使⽤
    SynchronousQueue 作为⼯作队列;
  3. newFixedThreadPool(int nThreads):重⽤指定数⽬(nThreads)的线程,其背后使⽤的是⽆界的⼯
    作队列,任何时候最多有 nThreads 个⼯作线程是活动的。这意味着,如果任务数量超过了活动队列
    数⽬,将在⼯作队列中等待空闲线程出现;如果有⼯作线程退出,将会有新的⼯作线程被创建,以补
    ⾜指定的数⽬ nThreads;
  4. newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进
    ⾏定时或周期性的⼯作调度;
  5. newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建
    的是个 ScheduledExecutorService,可以进⾏定时或周期性的⼯作调度,区别在于单⼀⼯作线程还是
    多个⼯作线程;
  6. newWorkStealingPool(int parallelism):这是⼀个经常被⼈忽略的线程池,Java 8 才加⼊这个创建⽅
    法,其内部会构建ForkJoinPool,利⽤Work-Stealing算法,并⾏地处理任务,不保证处理顺序;
    ThreadPoolExecutor():是最原始的线程池创建,上⾯1-3创建⽅式都是对ThreadPoolExecutor的封
    装。

面试题 线程池中 submit()和 execute()⽅法有什么区别?

execute():只能执⾏ Runnable 类型的任务。
submit():可以执⾏ Runnable 和 Callable 类型的任务。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秀才大大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值