自学内容网 自学内容网

【JAVA EE】多线程、锁、线程池的使用

目录

创建线程

方法一:继承Thread类来创建一个线程类

方法二:实现Runnable,重写run

线程等待

获取当前线程引用

休眠当前线程

线程的状态

synchronized

synchronized的特性

1、互斥

2、刷新内存

死锁

死锁的四个必要条件

避免死锁

volatile关键字

wait方法

notify方法

定时器

线程池 

线程池的创建

往线程池里添加任务


创建线程

方法一:继承Thread类来创建一个线程类

class MyThread extends Thread{
    @Override
    public void run() {
        while(true)
        {
            System.out.println("1111");
        }
    }
}

public class Main {

    public static void main(String[] args) {

        //创建Mythread类的实例
        MyThread myThread=new MyThread();

        //调用start方法启动线程
        myThread.start();
    }

继承Thread,使用匿名内部类

Thread t =new Thread(){
    @Override
    public void run() {
        System.out.println("111");
    }
};
t.start();

方法二:实现Runnable,重写run

class MyRunnable implements  Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Main {

    public static void main(String[] args) {
        Runnable runnable=new MyRunnable();
        Thread t =new Thread(runnable);
        t.start();
    }
}

使用Runnable和继承Thread方法的区别在于:解耦合

TIPS

.start 和 .run方法的区别:

.run代码中不会创建出新的线程,只有一个主线程,这个主线程里面只能依次执行循环,执行完一个循环再执行另一个

我们可以使用IDEA或者jconsole来观察进程里多线程的情况

jconsole是jdk自带的工具,我们在jdk的bin目录里可以找到 

在启动jconsole前,确保idea程序已经跑起来了,如下会列出当前机器上运行的所有java进程

Thread-0就是我们新建的线程

 如果要更明显的找到新线程,我们在创建线程的时候,给它设置一下名字

Thread t =new Thread(()->{
    while(true){
        System.out.println("hello");
    }
},"这是新线程名字");
t.start();

线程属性

属性获取方法
IDgetid()
名称

getName()

状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isinterrupted()
  • 这里的id是java分配的,不是系统api提供的
  • 后台线程不结束,不影响整个进程结束,设置后台线程 t.setDaemon(true)
  • 前台线程没有执行结束,整个进程是一定不会结束的
  • isAlive判定内核线程是不是已经没了,回调方法执行完毕,线程就没了,但是Thread对象可能还在。

lambda表达式有一个语法规则,变量捕获,是可以自动捕获到上层作用域中涉及到的局部变量,所谓变量捕获,就是让lambda表达式,把当前作用域中的变量在lambda内部复制了一份

只能捕获一个final或者实际上是final的常量(变量没有使用final,但是没有进行修改)

在java中并不能像C++一样,直接中断一个线程,只能让线程任务做的快一点,依靠标志位来决定,具体示例如下:

Thread t =new Thread(()->{
    while(!Thread.currentThread().isInterrupted()){
        System.out.println("hello");
    }
},"这是新线程名字");
t.start();
System.out.println("让t线程结束");
t.interrupt();

设置标志位的相关方法

方法说明

public void interrupt()

中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
publi static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isinterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位

线程等待

让一个线程等待另一个线程执行结束,再继续执行,本质就是控制线程结束的顺序

join线程等待核心方法

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等millis毫秒
public void join(long millis,int nanos)等待时间精度更高

主线程中,调用t.join() 此时就是主线程等待t线程先结束

Thread t =new Thread(()->{
    for(int i=0;i<5;i++)
    {
        System.out.println("t线程工作");
    }
},"这是新线程名字");
t.start();
System.out.println("主线程开始等待");
t.join();
System.out.println("主线程等待结束");

一旦调用join,主线程就会发生阻塞,一直阻塞到t执行完毕了(没有设置等待时间的话),join才会解除阻塞,主线程才会继续执行 

获取当前线程引用

方法说明
public static Thread currentThread()返回当前线程对象的引用

休眠当前线程

方法说明
public static void sleep(long millis)休眠当前线程millis毫秒
public static void sleep(long millis,int nanos)更高精度

线程的状态

JAVA中有六个线程状态:

  • NEW:安排了任务,还未开始行动
  • RUNNABLE:可工作的,又可以分为正在工作中和即将开始工作(也就是就绪状态)
  • BLOCKED:排队等待,由于锁竞争导致的阻塞
  • WAITING:排队等待,由wait这种不固定时间的方式产生的阻塞
  • TIMED_WAITING:排队等待,由于sleep这种固定时间的方式产生的阻塞
  • TERMINATED:工作完成了,Thread对象还在,内核中的线程已经没了

获取线程状态可以通过getState方法,如下:

System.out.println(t.getState());
t.start();
t.join();
System.out.println(t.getState());

synchronized

因为线程的调度顺序是不确定的,这会导致线程不安全的情况,因此我们需要引入锁,再Java的一个对象对应的内存空间中,除了自己定义的一些属性之外,还有一些自带的属性,对象头,对象头中,其中就有属性表示当前对象是否已经加锁

synchronized的特性

1、互斥

synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象,synchronized就会阻塞等待。

  • 进入synchronized修饰的代码块,相当于加锁
  • 退出synchronized修饰的代码块,相当于解锁

synchronized除了修饰代码块之外,还可以修饰一个实例方法,或者修饰一个静态方法

对于普通方法,有两种写法:

第一种:

synchronized public void increase()
{
    count++;
}

第二种:

public void increase()
{
    synchronized (this)
    {
        count++;
    }
}

这两种方法是一样的,第一种可以看成第二种的简写 。

如果是修饰静态方法,相当于是针对类对象加锁

class Counter{
    public int count;

    //第一种方法
    synchronized public static void increase()
    {

    }

    //第二种方法
    public static void increase()
    {
        synchronized (Counter.class)
        {
            count++;
        }
    }
}

 两种方法也是等价的。

2、刷新内存

synchronized的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量到最新副本工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

当出现如下代码:

synchronized (locker)

 {
       synchronized (locker){

        ……

        }
 }

3、可重入

       第一次加锁,假设能够加锁成功,此时的locker就属于是“被锁定的”状态,进行第二次加锁,很明显,locker已经是锁定状态了,第二次加锁操作,原则上是应该要“阻塞等待”的,应该要等待到锁被释放,才能加锁成功的, 但是实际上,一旦第二次加锁的时候阻塞了,就会出现死锁情况。

        因此synchronized的可重入性,也就是记录当前持有锁的线程,如果是持有锁的线程再进行加锁,则允许加锁。但是释放锁是要到最外层结束才释放,这里使用了引用计数,锁对象中,不仅要记录谁拿到了锁,还要记录,锁被加了几次,每加锁一次,计数器就+1,每解锁一次,计数器就-1,直到0,才真正释放锁

死锁

死锁的四个必要条件

  1. 互斥使用(锁的基本特性):当一个线程持有一把锁之后,另一个线程也想获取到锁,就需要阻塞等待
  2. 不可抢占(锁的基本特性):当锁已经被线程1拿到之后,线程2只能等线程1主动释放,不能强行抢占过来
  3. 请求保持(代码结构):一个线程尝试获取多把锁,先拿到锁1,再拿锁2的时候,锁1不释放
  4. 循环等待/环路等待:等待的依赖关系,形成环

避免死锁

解决死锁,破坏上面三、四条件即可

  • 对于3来说,调整代码结构,避免写“锁嵌套”逻辑
  • 对于4来说,可以约定加锁顺序,就可以避免循环等待,针对锁进行编号,比如约定加多把锁的时候,先加编号小的锁,后加编号大的锁

volatile关键字

1、保证内存可见性

        用volatile修饰的变量,每次都从内存中读取值,而不会因为编译器优化,将常访问未修改的变量值读取到寄存器,内存中对应变量值修改后,程序还是访问的寄存器(java中叫做工作内存)的值,从而导致“内存可见性”问题(也是线程安全的问题)。

        关于内存可见性,还涉及到一个关键概念,JMM(Java内存模型)

2、禁止指令重排序

TIPS

        volatile和synchronized都能对线程安全起到一定的积极作用,但是volatile不能保证原子性,而synchronized保证原子性,synchronized也能保证内存可见性。

wait方法

wait做的事情:

  • 使当前执行代码的线程进行等待,把线程放到等待队列中
  • 释放当前的锁
  • 满足一定条件时被唤醒,重新尝试获取这个锁

notify方法

notify时唤醒等待的线程

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,对其发出通知,并使它们重新获取该对象的锁
  • 如果多个线程等待,则由线程调度器随机挑选出一个wait状态的线程(没有先来后到)

定时器

在标准库中java.util.Timer

如下是一个简单的使用示例:

主线程执行schedule方法的时候,就是把这个任务给放到timer对象中,与此同时,timer里头也包含一个线程,这个线程叫做“扫描线程”,一旦时间到,扫描线程就会执行刚才安排的任务了,Timer里可以安排多个任务的。

Timer timer=new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("时间到了");
    }
},2000);
System.out.println("程序开始");

此处传参使用匿名内部类的写法,继承了TimerTask,并且创建出一个实例,TimerTask实现了Runnable接口,以此来重写run方法,通过run描述任务的详细情况。

public abstract class TimerTask implements Runnable 

线程池 

线程池就是把线程创建好,放到池子里,后续用的时候直接从池子里取,这样效率会比我们每次都新创建线程,效率更高。

那么为什么从线程池里取的效率会比新创建线程效率更高呢?

线程池的创建

线程池对象不是我们直接new的,而是通过一个专门的方法,返回一个线程池对象,如下:

ExecutorService service= Executors.newCachedThreadPool();

这种方法就是采用工厂模式 ,Executors是一个工厂类,newCachedThreadPool()是一个工厂方法。

newCachedThreadPool线程池里的线程用过之后不着急释放,以备随时再使用,此时构造出来的线程池对象,有一个基本的特点:线程数目是能够动态适应的,随着往线程池里添加任务,这个线程中的线程会根据需要自动被创建出来。

newFixedThreadPool这个工厂方法就是创建一个固定大小的线程池,如下是创建一个包含3个线程的线程池

ExecutorService service= Executors.newFixedThreadPool(3);

除了上述两种工厂方法,还有 newScheduledThreadPool(),newSingleThreadExecutor(),这两种并不常用,这里便不再介绍。

这几个工厂方法生成的线程池,本质上都是对一个类(ThreadPoolExecutor)进行的封装。

ThreadPoolExecutor构造参数:

  • int corePoolSize 核心线程数
  • int maximumPoolSize 最大线程数

这两个参数描述了线程池中,线程的数目,这个线程池里线程的数目是可以动态变化的,变化范围就是[corePoolSize,maximumPoolSize]

  • long keepAliveTime  允许非核心线程数存留时间
  • TimeUnit unit 时间单位 ms,s,min
  • BlockingQueue<Runable> workQueue 阻塞队列,用来存放线程池中的任务,可以根据需要,灵活设置这里的队列是啥,需要优先级就可以设置PriorityBlockingQueue,如果不需要优先级,并且任务数目是相对恒定的,可以使用ArrayBlockingQueue,如果不需要优先级,并且任务数目变动较大的,使用LinkedBlockingQueue
  • ThreadFactory threadFactory 工厂模式的体现,不用手动设置属性
  • RejectedExecutionHandler handler 线程池的拒绝策略,一个线程池能容纳的任务数量,有上限,当到达上线的时候,做出什么样的反应,如下是常用拒绝策略:
ThreadPoolExecutor.AbortPolicy直接抛出异常
ThreadPoolExecutor.CallerRunsPolicy新添加的任务,由添加任务的线程负责执行
ThreadPoolExecutor.DiscardOldestPolicy丢弃任务队列中最老的任务
ThreadPoolExecutor.DiscardPolicy丢弃当前新加的任务

常见问题:

使用线程池,需要设置线程的数目,数目设置多少合适?

        使用实验的方式,对程序进行性能测试,测试过程中尝试修改不同的线程池的线程数目,看哪种情况更符合要求

往线程池里添加任务

通过submit往线程池里注册任务

service.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
    }
});


原文地址:https://blog.csdn.net/lzb_kkk/article/details/143199023

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!