Java线程

Posted hnu你深

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java线程相关的知识,希望对你有一定的参考价值。

目录

一、线程与进程

二、线程调度

三、同步与异步

四、并发与并行

五、Java线程的使用

1、继承Thread

2、实现Runnable

 3、通过匿名内部类实现

六、Thread常用方法 

1、设置和获取线程名称 

 2、线程休眠sleep

 3、线程阻塞

 4、线程中断

七、线程的六种不同状态

八、守护线程

九、线程安全

1、为什么会出现线程安全问题?  

2、 解决方案一:同步代码块sychronized

3、 解决方案二:同步方法

4、解决方案三:显式锁

5、Java公平锁和非公平锁

6、Java线程死锁

十、Java多线程通信问题

十一、带返回值的线程Callable

1、Runnable 与 Callable

2、Callable使用步骤

3、Runnable 与 Callable的相同点

4、Runnable 与 Callable的不同点

5、Callable获取返回值

6、FutureTask类

十二、Java线程池

1、线程池 Executors

2、线程池的好处

3、Java中的四种线程池 . ExecutorService

十三、Lambda表达式


一、线程与进程

进程

  • 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间 。

线程

  • 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少有一个线程。
  • 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。

二、线程调度

分时调度

  • 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

抢占式调度

  • 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
  • CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻, 只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

三、同步与异步

同步:排队执行 , 效率低但是安全。

异步:同时执行 , 效率高但是数据不安全。

四、并发与并行

并发:指两个或多个事件在同一个时间段内发生。

并行:指两个或多个事件在同一时刻发生(同时发生)。

五、Java线程的使用

1、继承Thread

 运行结果:

  • 第一次:

  •  第二次:

 程序线程执行的时序图:

2、实现Runnable

 运行结果:

实现 Runnable与继承Thread比较

 3、通过匿名内部类实现

 运行结果:

六、Thread常用方法 

1、设置和获取线程名称 

 2、线程休眠sleep

每隔一秒输出一次:

 3、线程阻塞

 https://blog.csdn.net/sunshine_2211468152/article/details/87299708

https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B%E9%98%BB%E5%A1%9E/2233470?fr=aladdin

4、线程中断

 修改run方法,发现中断标记后自杀:(可以先释放占用的资源后自杀)

七、线程的六种不同状态

 

 详见:https://blog.csdn.net/pange1991/article/details/53860651

八、守护线程

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

  • 用户线程:当一个进程不包含任何的存活的用户线程时,进程结束。
  • 守护线程:守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡。

代码示例:

 代码改造:

 运行结果:

九、线程安全

1、为什么会出现线程安全问题?  

实例:

 运行结果出现了余票为负的情况

分析:

线程1线程2线程3count
while(count>0)就绪1
system.out.println("正在准备卖票")就绪1
Thread.sleep(1000)while(count>0)1
system.out.println("正在准备卖票")1
Thread.sleep(1000)1
while(count>0)1
system.out.println("正在准备卖票")1
Thread.sleep(1000)1
count--0
system.out.println("出票成功,余额:"+count)0
while(count>0)0
count---1
system.out.println("出票成功,余额:"+count)-1
count---2
system.out.println("出票成功,余额:"+count)-2

2、 解决方案一:同步代码块sychronized

要看同一把锁!

 把该任务分配给三个子线程,争夺同一个Object对象o的锁:排队执行

3、 解决方案二:同步方法

1、同步方法的锁是?

非静态方法:this

静态方法:类名.class

2、一个类里面有多个同步方法?

只要一个同步方法运行,其他同步方法就不能运行

4、解决方案三:显式锁

线程分配到同一个Ticket任务,争夺同一把锁l

5、Java公平锁和非公平锁

 锁Lock分为公平锁和非公平锁。

  • 公平锁:表示线程获取锁的顺序是按照加锁的顺序来分配的,及先来先得,先进先出的顺序。
  • 非公平锁:表示获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定能拿到锁,有可能一直拿不到锁,所以结果不公平。 

公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁, 否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。

非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式

以下内容转自链接:https://www.imooc.com/article/284585

(1)什么是非公平锁?

如上图,现在线程1加了锁,然后线程2尝试加锁,失败后进入了等待队列,处于阻塞中。然后线程1释放了锁,准备来唤醒线程2重新尝试加锁。

注意一点,此时线程2可还停留在等待队列里啊,还没开始尝试重新加锁呢!然而,不幸的事情发生了,这时半路杀出个程咬金,来了一个线程3!线程3突然尝试对ReentrantLock发起加锁操作,此时会发生什么事情?

很简单!线程2还没来得及重新尝试加锁呢。也就是说,还没来得及尝试重新执行CAS操作将state的值从0变为1呢!线程3冲上来直接一个CAS操作,尝试将state的值从0变为1,结果还成功了!一旦CAS操作成功,线程3就会将“加锁线程”这个变量设置为他自己。给大家来一张图,看看这整个过程:

明明人家线程2规规矩矩的排队领锁呢,结果你线程3不守规矩,线程1刚释放锁,不分青红皂白,直接就跑过来抢先加锁了。这就导致线程2被唤醒过后,重新尝试加锁执行CAS操作,结果毫无疑问,失败!

原因很简单啊!因为加锁CAS操作,是要尝试将state从0变为1,结果此时state已经是1了,所以CAS操作一定会失败!一旦加锁失败,就会导致线程2继续留在等待队列里不断的等着,等着线程3释放锁之后,再来唤醒自己,真是可怜!先来的线程2居然加不到锁!

同样给大家来一张图,体会一下线程2这无助的过程:

上述的锁策略,就是所谓的非公平锁

如果你用默认的构造函数来创建ReentrantLock对象,默认的锁策略就是非公平的。在非公平锁策略之下,不一定说先来排队的线程就就先会得到机会加锁,而是出现各种线程随意抢占的情况。那如果要实现公平锁的策略该怎么办呢?也很简单,在构造ReentrantLock对象的时候传入一个true即可:

ReentrantLock lock = new ReentrantLock(true)

此时就是说让他使用公平锁的策略,那么公平锁具体是什么意思呢?

(2)什么是公平锁?

咱们重新回到第一张图,就是线程1刚刚释放锁之后,线程2还没来得及重新加锁的那个状态。

同样,这时假设来了一个线程3,突然杀出来,想要加锁。

如果是公平锁的策略,那么此时线程3不会跟个愣头青一样盲目的直接加锁。他会先判断一下:咦?AQS的等待队列里,有没有人在排队啊?如果有人在排队的话,说明我前面有兄弟正想要加锁啊!如果AQS的队列里真的有线程排着队,那我线程3就不能跟个二愣子一样直接抢占加锁了。因为现在咱们是公平策略,得按照先来后到的顺序依次排队,谁先入队,谁就先从队列里出来加锁!所以,线程3此时一判断,发现队列里有人排队,自己就会乖乖的排到队列后面去,而不会贸然加锁!

同样,整个过程我们用下面这张图给大家直观的展示一下:

上面的等待队列中,线程3会按照公平原则直接进入队列尾部进行排队。接着,线程2不是被唤醒了么?他就会重新尝试进行CAS加锁,此时没人跟他抢,他当然可以加锁成功了。然后呢,线程2就会将state值变为1,同时设置“加锁线程”是自己。最后,线程2自己从等待队列里出队。

整个过程,参见下图:

 这个就是公平锁的策略,过来加锁的线程全部是按照先来后到的顺序,依次进入等待队列中排队的,不会盲目的胡乱抢占加锁,非常的公平。
 

6、Java线程死锁

(1)死锁的定义

     多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

     所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

(2)死锁产生的原因

1、系统资源的竞争

    通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。

2、进程推进顺序非法

    进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。

  从上面两个例子中,我们可以得出结论,产生死锁可能性的最根本原因是:线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,也就是说在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。另一个原因是默认的锁申请操作是阻塞的

3、死锁产生的必要条件:

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

(1)互斥:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

(2)不剥夺:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

(3)请求和保持:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

(4)循环等待:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有,如图1所示。

(3)产生死锁的实例

看下面一段代码:

public class DeadLock {

    public static void main(String[] args) {
        Police p = new Police();
        CulPrit c = new CulPrit();
        new MyThread(c,p).start();
        c.say(p);
    }

    static class MyThread extends Thread {

        private CulPrit c;
        private Police p;

        public MyThread(CulPrit c,Police p) {
            this.c = c;
            this.p = p;
        }

        @Override
        public void run() {
            p.say(c);
        }
    }

     static class Police {

        public synchronized void say(CulPrit c) {
            System.out.println("警察:你放走人质,我放走你");
            c.fun();
        }

        public synchronized void fun() {
            System.out.println("警察:罪犯放走了人质,警察也放走了罪犯");
        }

    }

     static class CulPrit{

        public synchronized void say(Police p){
            System.out.println("罪犯:你放走我,我放走人质");
            p.fun();
        }

        public synchronized void fun(){
            System.out.println("罪犯:警察放走了罪犯,罪犯也放走了人质");
        }
    }

}

运行结果:警察和罪犯互相等待对方的回复fun,但是警察的say占用了this锁,罪犯的say也占用了this锁。

运行多次后出现了另外一种输出:此时在子线程执行p.say(c)之前主线程抢先执行了p.fun(),程序正常结束。

 

 该部分参考链接:

https://www.cnblogs.com/xiaoxi/p/8311034.html

十、Java多线程通信问题

 Object类的一些方法:

voidnotify()

唤醒正在此对象监视器上等待的单个线程。

voidnotifyAll()

唤醒等待此对象监视器的所有线程。

voidwait()

导致当前线程等待它,通常是 通知中断

voidwait​(long timeoutMillis)

导致当前线程等待它,通常是 通知中断 ,或者直到经过一定量的实时。

voidwait​(long timeoutMillis, int nanos)

导致当前线程等待它,通常是 通知中断 ,或者直到经过一定量的实时。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Demo4  {

    /**
     * 多线程通信问题, 生产者与消费者问题
     * @param args
     */
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }

    //厨师
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f) {
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                if(i%2==0){
                    f.setNameAndSaste("老干妈小米粥","香辣味");
                }else{
                    f.setNameAndSaste("煎饼果子","甜辣味");
                }
            }
        }
    }
    //服务生
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f) {
            this.f = f;
        }
        @Override
        public void run() {
            for(int i=0;i<100;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }
    //食物
    static class Food{
        private String name;
        private String taste;

        public synchronized void setNameAndSaste(String name,String taste){
            if(flag) {
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
            }
        }
        public synchronized void get(){
            System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste);
        }
    }
}

运行结果:

 服务员端走的菜口味与厨师设置的口味不一样。

分析:i%2==0时厨师做完老干妈小米粥,还没等到服务员端走菜,又“回手掏” 开始了做菜,这次i%2==1厨师setName("煎饼果子")然后睡眠100ms这时被服务员线程抢占了锁,服务员把菜端走了,因此菜品的名称是“煎饼果子”,口味却是”香辣味“。

这里厨师是生产者线程,而服务员是消费者线程,生产者生产一个单位的产品还未结束,就被消费者线程抢先运行,导致了产品的数据错乱。

对Food进行修改:

 运行结果:菜品数据皆正确

十一、带返回值的线程Callable

1、Runnable Callable

2、Callable使用步骤

3、Runnable Callable的相同点

4、Runnable Callable的不同点

5、Callable获取返回值

        Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

6、FutureTask类

构造器描述
FutureTask​(Runnable runnable, V result)

创建一个 FutureTask ,在运行时执行给定的 Runnable ,并安排 get在成功完成时返回给定的结果。

FutureTask​(Callable<V> callable)

创建一个 FutureTask ,在运行时将执行给定的 Callable

变量和类型方法描述
protected voiddone()

当此任务转换到状态 isDone (无论是正常还是通过取消),调用受保护的方法。

Vget()

如果需要等待计算完成,然后检索其结果。

Vget​(long timeout, TimeUnit unit)

如果需要,最多等待计算完成的给定时间,然后检索其结果(如果可用)。

protected booleanrunAndReset()

执行计算而不设置其结果,然后将此未来重置为初始状态,如果计算遇到异常或被取消则无法执行此操作。

protected voidset​(V v)

将此future的结果设置为给定值,除非已设置或已取消此未来。

protected voidsetException​(Throwable t)

导致此未来报告带有给定throwable的ExecutionException作为其原因,除非此未来已设置或已取消。

StringtoString()

返回此FutureTask的字符串表示形式。

 实例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class DemoCallable {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        Integer j = futureTask.get();//获取返回值
        System.out.println(j);
        for(int i=0;i<10;i++){
            try{
                Thread.sleep(100);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println(i);
        }

    }

    static class MyCallable implements Callable{

        @Override
        public Integer call() throws Exception {
            for(int i=0;i<10;i++){
                try{
                    Thread.sleep(100);
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println(i);
            }
            return 100;
        }
    }
}

 运行结果:可以看到在子线程运行结束以后主线程才执行后续的代码

运行结果

十二、Java线程池

1、线程池 Executors

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。

2、线程池的好处

  • 降低资源消耗。
  • 提高响应速度。
  • 提高线程的可管理性。

3、Java中的四种线程池 . ExecutorService

(1) 缓存线程池

/*** 缓存线程池. 
   * (长度无限制) 
   * 执行流程: 
   * 1. 判断线程池是否存在空闲线程 
   * 2. 存在则使用 
   * 3. 不存在,则创建线程并放入线程池, 然后使用 
*/ 
ExecutorService service = Executors.newCachedThreadPool(); 
 
//向线程池中加入新的任务 
service.execute(new Runnable() { 
    @Override 
    public void run() { 
        System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    } 
}); 

service.execute(new Runnable() {
    @Override 
    public void run() { 
        System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    } 
}); 

service.execute(new Runnable() { 
    @Override 
    public void run() { 
        System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    } 
});

(2) 定长线程池

/**
* 定长线程池。
* (长度是指定的数值)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程并放入线程池,然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*/

Executorservice service = Executors.newFixedThreadPool(2) ;

service.execute(new Runnable() {
    @override
    pub1ic void run() {
        System.out.println("线程的名称:"+Thread.currentThread().getName();
    }
});

service.execute(new Runnable() {
    @override
    pub1ic void run() {
        System.out.println("线程的名称:"+Thread.currentThread().getName()) ;
    }
});

3. 单线程线程池

效果与定长线程池创建时传入数值1效果-致.
/** 
* 单线程线程池. 
* 执行流程:
* 1. 判断线程池的那个线程是否空闲
* 2. 空闲则使用
* 3. 不空闲, 则等待池中的单个线程空闲后使用
*/
ExecutorService service = Executors.newsingleThreadexecutor();

service.execute(new Runnable(){
    @override
    public void run() {
        System.out.println("线程的名称:"+Thread.currentThread().getName());
    }
});

service.execute(new Runnable() {
    @override
    public void run() {
        System.out.println("线程的名称:"+Thread.currentThread().getName()) ;
    }
});

4. 周期性任务定长线程池

pub1ic static void main(String[] args) {
    /**
    * 周期任务定长线程池.
    * 执行流程:
    * 1. 判断线程池是否存在空闲线程
    * 2. 存在则使用
    * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程并放入线程池,然后使用
    * 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
    * 周期性任务执行时:
    * 定时执行,当某个时机触发时,自动执行某任务.
    */

    SchedledExecutorservice service = Executors.newscheduledThreadPool(2) ;

   /**
     * 定时执行
     * 参数1.    runnable类型的任务
     * 参数2.    时长数字
     * 参数3.    时长数字的单位
     */

    /*service.schedule(new Runnable(){
         @override
         public void run() {
             System.out.println("俩人相视一笑~嘿嘿嘿");
         },5,TimeUnit.SECONDS);
    */

    /**
     *周期执行
     *参数1.   runnable类型的任务
     *参数2.   时长数字(延迟执行的时长)
     *参数3.   周期时长(每次执行的间隔时间)
     *参数4.   时长数字的单位
     */
    service.scheduleAtFixedRate(new Runnable(){
        @override
        public void run(){
            System.out.println("俩人相视一笑~嘿嘿嘿") ;
        },5,2,TimeUnit.SECONDS);
}

十三、Lambda表达式

使用方法: 

面向对象的一般代码: 

 使用Lambda表达式:

以上是关于Java线程的主要内容,如果未能解决你的问题,请参考以下文章

Java工程师面试题,二级java刷题软件

java线程

Java——线程池

Java线程池详解

Java线程池详解

Java 线程池详解