java SE -- 线程 asset

一.进程

进程,是正在运行的程序实例,是操作系统进行资源分配的最小单位。每个进程都有它自己的地址空间和系统资源(比如CPU时间,内存空间,磁盘IO等)。多个进程可以同时执行,每个进程在运行时都不会影响其他进程的运行,资源不共享;

程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

二.线程

2.1线程的简介

2.1.1线程和进程的区别

线程是进程的一部分,是CPU能够进行运算调度的最小单位。线程不能独立存在,必须依赖于进程。线程是一个进程中的顺序执行流(执行单元)。一个进程中可以有一个线程,也可能有多个线程。每个线程都有自己的指令指针、堆栈和局部变量等,但他们共享进程的代码,数据和全局变量等资源

多线程可以实现并发执行,提高程序的效率。

线程和进程的区别:

1.进程是操作系统运行的一个任务,线程是进程中的一个任务

2.进程是资源分配的最小单元,线程是程序执行的最小单元

3.线程是轻量级的进程,一个进程中包含多个线程,多线程共享进程中的数据,使用相同的地址空间,因此,线程间的通信更加方便,CPU切换一个线程的开销比进程小很多

4.一个进程结束,其内部的所有线程都会结束,但是不会对另外的进程造成影响。多线程程序,一个线程结束,可能会造成其他的线程结束

2.1.2  CPU时间片

CPU时间片在单核处理器上时,一次只能运行某一个进程的某一个线程,如何公平处理,一种方法就是引入时间片的概念,让每个程序轮流执行。

CPU调度机制算法:会将时间划分成一个个的时间片,时间片的大小从几ms到几百ms;

线程调度:线程调度是计算机多线程操作系统中分配CPU时间给各个线程的过程。每个线程代表程序中的一个执行路径,操作系统通过线程调度器分配处理器时间,决定哪个线程将获得执行的机会,以及获得的时间长短。

进程调度:进程调度是操作系统中分配CPU时间给各个进程的过程。进程是系统进行资源分配和调度的独立单位,它包含代码、数据以及分配的系统资源。与线程调度不同,进程调度涉及到的上下文切换成本更高,因为进程间共享的资源更少。

串行和并发:

串行指同步运行,并行指异步运行

操作系统将时间划分成很多时间片段,尽可能的均匀分配给每一个线程,获取时间片段的线程被CPU运行,而其他线程处于等待状态。所以这种微观上是走走停停,断断续续的,宏观上都在运行的现象叫并发。

2.2线程的调度机制

2.2.1 Java线程的状态简介

在java中,线程可以处于一下几种状态;

新建状态(New):线程对象已经创建,但还没有调用start()方法。

就绪状态(Runnable):线程已经调用start()方法,等待CPU调度执行。

运行状态(Running):线程获得CPU时间片,开始执行run()方法里的代码。

阻塞状态(Blocked):线程因为某些原因放弃CPU使用权,暂时停止运行,直到进入就绪状态。

等待状态(Waiting):线程因为某些条件而进入等待状态,此时不会被分配CPU时间片,直到其他线程显式地唤醒。

超时等待状态(Timed Waiting):线程在指定的时间内等待,时间到后会自动返回到就绪状态。

终止状态(Terminated):线程的run()方法执行完毕或者因异常退出而结束线程的生命周期。

2.2.2 抢占式调度与协同式调度

java线程的调度基本上是抢占式的,在这种模式下,每个java线程都有机会获得时间片,操作系统基于线程的优先级来决定哪个线程会优先运行。高级的线程会获得更多的运行机会。

而协同式调度需要线程主动释放控制权,当前线程必须要主动让出CPU时间,其他的线程才能有执行的机会,导致协同式调度在java中出现得较少。

2.2.3 线程的优先级

线程的切换是由线程调度来控制的,我们无权通过代码来干涉,但是可以通过设置线程的优先级来提高线程获取时间片段的概率。通过setPriority来设置优先级,优先级较高的线程有更大概率获得CPU时间片。

线程的优先级分为1-10,1最低,10最高,线程内部提供了3个关键字来表示最低,最高,默认优先级。

Thread.MIN_PRIORITY  表示最低优先级

Thread.MAX_PRIORITY  表示最高优先级

Thread.NORM_PRIORITY  表示默认优先级

2.2.4 线程生命周期管理

java虚拟机可以通过Thread类的方法来管理线程的生命周期,比如start()、sleep()、yield()、join()、wait()等,让线程在恰当的位置运行或者暂停。

start(  )方法可以使线程处于就绪状态

yield(  )方法可以使当前运行的线程让出自己的时间片,但不会阻塞线程,只是将线程从运行状态转移至就绪状态。

join(  )方法可以让一个线程等待另一个线程完成后再继续执行。

sleep(  )方法可以使当前线程暂停执行指定时间。

wait(  )方法是当前线程释放锁,释放cpu等资源,进入等待状态

3.线程的创建及其常用的API

3.1 线程的三种创建方式

3.1.1第一种

使用Thread的实现类或者匿名内部类的方式创建一个线程;

1.实现Thread

class MyThread extends Thread{
    //重写run方法:  run方法就是用来编写线程的任务代码的
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

我们自定义一个类,继承Thread类,并重写里面的run方法, run方法就是用来编写线程的任务代码的,我们需要实现1-100的打印,所以设置一个for循环打印即可,在sout中,需要传入Thread.currentThread( ).getName( )来获取到当前线程的名字,后面加上 i 即可

 public static void main(String[] args) {
        //创建一个线程对象,处于新建状态
        MyThread mt = new MyThread();
        //启动线程,使其处于就绪状态
        mt.start();

之后我们就可以在main方法中创建一个对象;然后通过对象来调用start方法,让线程启动。

2.使用匿名内部类的方式:

Thread t2 = new Thread(){
            public void run(){
                for(int i=0; i<100; i++){
                    System.out.println("hello world");
                }
            }
        };
        //启动线程t2,使其处于就绪状态
        t2.start();
        System.out.println("-----main结束-----");

我们可以直接在main方法中创建一个匿名内部类对象来创建线程,直接在花括号中重写run方法,确定线程任务行为;打印100次hello world;之后同样需要启动线程。

3.1.2 第二种

使用Runnable的实现类或者匿名内部类的方式创建一个线程;

1.实现Runnable接口

class MyTask implements Runnable {
    //重写run方法:线程的任务代码
    public void run() {
        int sum = 0;
        for (int i = 0; i < 101; i++) {
            if (i % 2 == 0) {
                sum += i;
            }
        }
        System.out.println("sum: "+sum);
    }
}

我们在自定义的类实现Runnable接口,重写里面的run方法,也就是任务所需的代码,我们希望打印出100以内的偶数和,

 public static void main(String[] args) {
        //获取Runnable的实例对象
        Runnable task = new MyTask();
        //创建线程对象,调用构造器Thread(Runnable runnable),传入任务
        Thread t1 = new Thread(task);
        //启动线程,进入就绪状态
        t1.start();

之后在main方法中获取Runnable的实例化对象,然后创建一个线程对象,将Runnable的实例化对象传入,然后用线程对象启动线程就可以了;

2.使用匿名内部类的方式

Runnable r = () ->{
            int sum = 0;
            for(int i=0; i<100; i++){
                if(i%2==1){
                    sum+=i;
                }
            }
            System.out.println("sum="+sum);
        };
        Thread t2 = new Thread(r);
        t2.start();
    }
}

我们可以用Lambda的表达式()用于传递形参,{ }中写run方法的内容,我们希望打印100以内所有的奇数和,然后实例化一个线程对象,形参传入Runnable对象,并调用启动方法。

3.1.3 第三种

先获取一个Callable对象,重写里面的call方法,call相当于run,但是call方法有返回值,

之后获取一个FutureTask对象,将上面的Callable对象传入构造器,

之后获取Thread对象,将上面的FutureTask对象传入构造器;

public static void main(String[] args) throws ExecutionException, InterruptedException {
        //Callable 是函数式接口,里面的 V call () 相当于Thread或者Runnable的run方法,即任务代码书写的位置
        Callable c1 = ()->{
            int sum = 0;
            for (int i = 2; i <=100 ; i+=2) {
                sum+=i;
                Thread.sleep(100);
            }
            return sum;
        };

        //调用FutureTask的构造器,传入Callable对象
        FutureTask<Integer> ft = new FutureTask<>(c1);
        //创建Thread线程对象,调用start方法进入就绪状态
        Thread thread = new Thread(ft);
        thread.start();
        //获取线程结束之后的结果   注意:get方法有阻塞所在线程的效果
        Integer result = ft.get();
        System.out.println("result: "+result);
        System.out.println("=====main方法结束======");
    }

Callable的返回值返回后,我们把对象传入FutureTask构造器,创建出的对象传入Thread构造器;让对象调用启动方法,获取Callable返回的结果;并将结果打印出来。

3.2Thread的常用构造器

3.2.1  Thread(Runnable runnable)

我们在形参中传入一个Runnable的任务对象;

Runnable r =  ()-> {
               int a1 = (int)(Math.random()*100);
               int a2 = (int)(Math.random()*100);
               int a3 = (int)(Math.random()*100);
               int a4 = (int)(Math.random()*100);
               int a5 = (int)(Math.random()*100);
            int[] arr = {a1,a2,a3,a4,a5};
            //冒泡排序
            for (int i = 0; i < arr.length-1; i++) {
                   for (int j = 0; j < arr.length-1-i; j++) {
                       if (arr[j] > arr[j+1]) {
                           int temp = arr[j];
                           arr[j] = arr[j+1];
                           arr[j+1] = temp;
                       }
                   }
            }
            System.out.println(Arrays.toString(arr));
        };
        new Thread(r).start();
        System.out.println("==========单参结束==========");

首先可以通过Lambda表达式创建好一个任务对象,先创建一个有随机数的数组,然后对这个数组进行冒泡排序,然后打印该数组;之后可以调用new关键字创建线程对象,传入任务对象,之后调用start启动该线程。

3.2.2  Thread(Runnable target,String name)

传入的第一个形参是任务接口的对象,第二个形参是创建出的线程名字;

Thread abc = new Thread(r, "abc");
        abc.start();
        System.out.println("=========双参结束=========");

任务接口就传入我们刚才创建的任务r就可以,命名是线程对象为abc,然后通过线程对象abc来调用start方法来启动线程。

就会得到第二个冒泡排序的数组。

3.2.3  Thread(String name)

形参是创建线程对象的名字,

Thread xiaohua = new Thread("xiaohua");

3.3常用的属性和方法

//1.获取当前线程的对象
        Thread current = Thread.currentThread();
        //2.获取当前线程的名字
        String name = current.getName();
        //3.获取当前线程的唯一标识符
        long id = current.getId();
        //4.获取当前线程的优先级
        int priority = current.getPriority();
        //5.获取当前线程的状态
        Thread.State state = current.getState();
        //6.查看当前线程是否存活
        boolean alive = current.isAlive();
        //7.查看当前线程是否被打断
        boolean interrupted = current.isInterrupted();
        //8.查看当前线程是否为守护进程
        boolean daemon = current.isDaemon();
        System.out.println("Current thread name: " + name);
        System.out.println("Current thread id: " + id);
        System.out.println("Current thread priority: " + priority);
        System.out.println("Current thread state: " + state.name());
        System.out.println("Current thread alive: " + alive);
        System.out.println("Current thread interrupted: " + interrupted);
        System.out.println("Current thread daemon: " + daemon);

main方法的本质上就是一个线程;我们可以调用main线程来查看相关的属性和方法,可以获取当前线程的对象并获取当前线程的名字,与当前线程的标识符,优先级。调用getState来获取当前线程的状态,判断当前线程是否存货,是否被打断,以及查看当前线程是否为守护线程。

3.4守护线程的说明

一个线程要么是守护线程,要么是前台线程:

前台线程:在表面运行的,能看到的,或者daemon的值为false的;

后台线程:就是守护线程,daemon的值为true的。

注意:当所有的前台线程都结束了,后台线程即使有任务,也要立即结束。

案例演示:

//第一个线程:rose喊10次 i jump  10次后,真跳了
        Thread rose = new Thread("rose"){
            public void run(){
                for(int i=1;i<=11;i++){
                    System.out.println(getName()+"说:I jump");
                    try{
                        Thread.sleep(500);
                    }catch(InterruptedException e){
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("------正在落水中------");
            }
        };

首先定义一个前台线程,rose在喊了10次 i jump 后,跳入大海,首先创建一个进程名字是rose,使用匿名内部类的方式创建进程,重写里面的run方法,循环打印10次 i jump,之后让程序休眠0.5秒再执行,需要使用try - catch 来捕获异常,在循环结束后,打印正在落水。

Thread jack = new Thread("jack"){
            public void run(){
                for(int i=1;i<=100;i++){
                    System.out.println(getName()+"说: you jump, I jump");
                    try{
                        Thread.sleep(500);
                    }catch(InterruptedException e){
                        throw new RuntimeException(e);
                    }
                }

            }
        };

之后设计第二个线程,名字是Jack,在循环打印100次 you jump , i jump 也让程序休眠0.5秒,此时我们应该判断哪个线程是守护线程,由于rose在喊完10次后会直接落水,此时jack就不应该继续喊了,所以jack应该是守护线程,当rose线程执行完后,jack应该立即停止执行。

//jack应该是守护进程,即rose跳水后,jack应该停止喊叫
        jack.setDaemon(true);
        rose.start();
        jack.start();

3.5生命周期相关方法

3.5.1 sleep()方法

线程的睡眠方法     static void sleep(long time),可以传入一个参数时间,单位是毫秒,让线程里的当前代码休眠,进入阻塞状态;不再占用CPU的时间片,休眠时间一过,就会立即进入就绪状态,等待调度器分配时间片段

注意:在休眠期间可能会被打断,因此要处理异常    InterruptedException

还有一个重载方法     static void sleep(long time,int nanos),该方法的第二个参数是指单位是纳秒

public static void main(String[] args) {
        Thread t1 = new Thread("download"){
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    System.out.println("正在下载视频中......"+(i*10)+"%");
                    //使用休眠方法来假装模拟正在下载中
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("========视频下载完成========");
            }
        };
        //测试
        t1.start();
    }

我们首先定义了一个线程名字是download,并使用匿名内部类的方式重写run方法循环打印10次下载过程来模拟现实生活中下载视频的过程。在每次打印下载过程中让程序休眠一段时间,需要捕获异常;

3.5.2 yield( )方法

线程的礼让方法    static void yield(): 表示让出CPU的时间片段,进入就绪状态,不过,下一个时间片段还有可能是该线程的

Thread t1 = new Thread("Thread-A"){
            public void run(){
                for(int i=0;i<=9;i++){
                    System.out.println(getName()+":"+i);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

首先我们定义了一个Thread-A,重写run方法,循环打印10次数字,并调用休眠方法每隔0.1秒打印一次,

Thread t2 = new Thread("Thread-B"){
            public void run(){
                for(int i=0;i<=9;i++){
                    //打印5之前,让一下时间片段
                    if(i==5){
                        //执行到28行,CPU正在被Thread-B使用
                        Thread.yield(); //这一行代码表示Thread-B让出CPU,进入就绪状态。下一个时间片段可能还会被线程调度器分配给Thread-B
                    }
                    System.out.println(getName()+":"+i);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

之后我们创建线程b,同样是打印10次数字,但是当i = 5时,让出时间片,进入就绪状态,但是下一个时间片段可能还会被线程调度器分配给线程B,同样让程序每隔0.1秒打印一次。

3.5.3 join( )方法

另一个线程加入方法  void join():此时当前线程会进入阻塞状态,等待另一个线程结束之后 ,当前线程会进入到就绪状态;

案例演示:模拟图片的下载和显示过程,应该先下载图片,再显示图片

Thread download = new Thread("download"){
            public void run() {
                for (int i = 1; i < 11; i++) {
                    System.out.println(getName() + "图片正在加载中... 进度条:" + i*10+"%");
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("图片下载完成");
            }
        };

首先我们定义下载线程,循环打印10次进度条,并让程序休眠0.3s执行

Thread show = new Thread("show"){
            public void run() {
                try {
                    //当前线程是显示线程,显示应该该下载线程执行完毕之后,再执行,因此要让download加入进来
                    download.join();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                for (int i = 1; i < 11; i++) {
                    System.out.println(getName() + "图片正在显示中... 进度条:" + i*10+"%");
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("图片显示完成...");
            }
        };

之后我们定义显示线程,重写任务代码中,我们需要考虑如果时间片段先分给了显示线程,需要让下载线程的代码都执行完毕才能继续执行显示线程,所以需要通过下载线程调用join方法,让下载线程加入,同样需要捕获异常,

3.5.4 interrupt()方法

线程中断,打断方法    void interrupt( );表示当前线程需要去打断另一个线程;

需要注意:是在当前线程中调用另一个线程的打断方法

public static void main(String[] args) {
        Thread lin = new Thread("林永健"){
            public void run() {
                System.out.println(getName()+"说:开始睡觉了");
                try {
                    //线程休眠100s,模拟林永健睡着了
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
//                    e.printStackTrace();
                    System.out.println(getName()+"说:干嘛呢?都破了相了");
                }
            }
        };

首先我们定义林永健让他开始睡觉,并设置休眠时间,

Thread huang = new Thread("黄宏"){
            public void run() {
                for (int i = 0; i < 10; i++) {
                    if (i == 0) {
                        Thread.yield();
                    }
                    System.out.println(getName() + "说:"+(i+1)+"个80" );
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println(getName()+"说:搞定了");
                //打断lin的睡觉
                lin.interrupt();
            }
        };

之后我们定义黄宏的线程,通过循环来打印几个八十,在打印完最后一个80时,需要通过lin的线程调用interrupt方法叫醒lin,

4.临界资源问题

4.1什么是临界资源

在一个进程之中,多个线程之间资源是共享的,如果一个资源同时被多个线程访问,那么这个资源就是临界资源,当多个线程同时并发读写一个临界资源问题时,就会发生线程并发安全问题。

常见的临界资源:

多线程共享实例变量        多线程共享静态公共变量

如果想解决线程安全问题,就需要将异步的操作变为同步的操作,

异步操作:相当于各干各的,多个线程并发

同步操作:操作有先后的顺序,相当于你干完我再干。

4.2锁机制

4.2.1锁机制的简介

针对于临界资源安全隐患问题的解决方式,引入了锁机制

1.锁机制的作用:将异步的代码块变成同步的代码块

2.语法:

synchronized(锁对象的地址){

             //需要同步的代码块(如果不同步,会出现安全隐患问题)

}

3.任何的java对象都可以作为锁,只有一个要求:所有的线程都是同一个对象即可。

4.同步的代码块尽量缩小范围,提高替他代码块的执行效率。

5.运行逻辑:

   当一个线程A执行到{ },说明该线程已经获取到了锁对象,其他的线程都必须等待,直到线程A执行完了同步代码块,会自动释放锁对象,其他的线程才有机会获取到锁对象,谁获取到锁对象,谁就能执行同步代码块,没获取到锁对象的线程继续等待。

4.2.2案例演示:

class Desk1 implements Runnable{
    //静态属性:豆子的数量,初始值10个
    private static int beanCount = 10;
    //拿走豆子,一次只能拿一个
    public void take(){
        beanCount--;
    }

首先我们定义一个类Desk1,有私有的静态变量10个豆子,并提供一个拿豆子的方法,

public void run() {
           while(beanCount!=0){  //桌子上只剩下一个豆子时,两个线程恰巧执行到条件判断。 1>0成立
               //那么两个线程都进入循环体了,各自拿走一个豆子,此时豆子就是-1个了,明显不合理
               //即出现了临界资源的安全隐患问题
               try {
                   //增加出现安全隐患的概率
                   Thread.sleep(500);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               //将具有安全隐患问题的代码放入{};因为两个线程操作的是同一个桌子,因此this可以作为锁对象
               synchronized(this){
                   if(beanCount>0){
                       take();
                   }
               }
               System.out.println(Thread.currentThread().getName()+"拿走了一个豆子,剩下的个数是"+beanCount);
               if(beanCount<=0){
                   break;
               }
       }
    }

之后我们重写任务代码,当桌子上的豆子只剩下一个时,如果有两个线程同时去拿豆子,那么最后的豆子数量会变成-1,这明显不合理,就是临界资源安全隐患问题,

我们需要将会出现安全隐患的代码块编程同步的,即给这部分代码块上锁,使用synchronized关键字,()中传入对象,在这里我们使用this,如果豆子的数量大于0,拿走豆子,当拿到最后一个豆子时,会有一个线程获取到锁对象,那么他会把豆子取走,等到下一个线程获取到锁对象时,豆子的数量是0,那么就不会进入循环。

public static void main(String[] args) {
        Desk1 desk1 = new Desk1();
        Thread xiaoming = new Thread(desk1,"小明");
        Thread xiaohong = new Thread(desk1,"小红");

        //启动线程
        xiaoming.start();
        xiaohong.start();
    }
}

在main方法中进行测试,首先创建一个Desk1对象,然后创建两个线程小明和小红,调用start方法进行测试。

class BlackBoard implements Runnable{
    private int apple = 10;
    public void run() {
        while(true){
            take();
            System.out.println(Thread.currentThread().getName()+"拿走了一个苹果,剩下的个数"+apple);
            if(apple == 0){
                break;
            }
        }
    }

    /**
     * 可以对非静态方法上锁,锁对象就是this
     */
    public synchronized void take(){
        if (apple >0){
            apple--;
        }
    }

}

我们可以设计另一个类,在黑板上拿走苹果,实现Runnable接口,先定义实例变量10个苹果,然后再定义一个非静态方法take(),并对非静态方法上锁,在void前面加上synchronized即可,之后重写run方法,并通过循环拿走苹果,如果苹果的个数变为0就跳出循环。

public static void main(String[] args) {
        BlackBoard bb = new BlackBoard();
        Thread xiaoming = new Thread(bb,"小明");
        Thread xiaohong = new Thread(bb,"小红");
        xiaohong.start();
        xiaoming.start();
    }

在main方法中进行测试,新建两个线程小明和小红,并启动线程;

这样就可以有效的避免出现临界资源隐患问题。

4.3  synchronized 的作用域

上锁的范围是需要同步的代码块范围,如果要给非静态方法上锁,需要在方法前添加修饰词synchronized,当有一个线程访问了该方法时,就相当于获得了锁对象,其他线程如果像访问该方法,就去要处于等待状态,有一个前提,多个线程访问的实例对象必须是同一个,当某一个线程正在访问一个同步方法时,this这个锁对象即被他占用,其他线程想要执行该实例对象的其他方法也需要等待,因为该方法的所对象已经被占用了

class Desk implements Runnable {
    //添加一个非静态属性,充当锁对象
    Object lock = new Object();
    private int BeanCount = 10;
    //如果给方法中的所有代码上锁,不如直接在方法上添加synchronized关键字,给方法上锁
    //此时不需要特意制定锁对象,因为锁对象是this
    public synchronized void take() {
        System.out.println("开始取豆子");
        //给部分代码上锁
      // synchronized (lock) { //lock作为锁
           if (BeanCount > 0) {
               BeanCount--;
           }
     //  }
        System.out.println("豆子被拿走了一个");
    }
    public void run() {
        while(BeanCount > 0){
            take();
        }
    }

    public synchronized void sport() {
        //...
    }
}

在上述代码中,我们添加一个非静态属性,作为锁对象,并提供私有的属性10个豆子,如果需要给方法中的所有代码都上锁,不如直接给方法上添加关键字来上锁,此时不需要指定锁对象,因为锁对象是this,之后就可以重写run方法,

public static void main(String[] args) {
        Desk desk = new Desk();
        Thread t1 = new Thread(desk,"小明");
        Thread t2 = new Thread(desk,"小红");
        t1.start();
        t2.start();
    }

我们在main方法中测试,先创建一个desk对象,然后创建两个线程,并启动线程。

4.4单例模式的改进

懒汉式单例,在多线程的环境下,会出现问题。由于临界资源问题的存在,单例对象可能会被实例化多次。解决方案,就是将对象的 null值判断和实例化上锁,作为同步代码块执行。

class Boss{
    //第一步:提供一个该类的私有的静态的该类的变量
    private static Boss boss;
    //第二步:将构造器私有化
    private Boss(){}
    //第三步:提供一个公有的静态的返回该类型的方法
    //给静态方法上锁,就是给方法添加修饰词synchronized
    //锁对象时类名.class,这个类对象在整个项目下都是唯一的
    public synchronized static Boss getInstance(){
//        synchronized (Boss.class){
            if(boss == null){
                boss = new Boss();
            }
//        }
        return boss;
    }


}

第一步先私有化静态变量,然后私有化构造器,提供返回值是该类型的公有方法,并对其上锁,如果对象的地址值是空,就新创建一个对象,返回该对象。这就是单例模式的懒汉模式。

4.5 死锁

死锁的产生原因:线程1先获取锁A,但是还想获取锁B,线程2获取了锁B,但还想获取锁A。两个线程都占用了对方想用的锁,而且对方还都占着锁不释放,因此都出现了等待现象,无法继续向下执行。

public static void main(String[] args) {
        Thread t1=new Thread("小明"){
            public void run(){
               synchronized("A"){
                   for (int i = 0; i <50 ; i++) {
                       System.out.println(getName()+":"+i);
                   }
                   synchronized("B"){
                       for (int i = 50; i < 100; i++) {
                           System.out.println(getName()+":"+i);
                       }
                   }
               }
            }
        };

        Thread t2=new Thread("小红"){
            public void run(){
                synchronized("B"){
                    for (int i = 0; i <50 ; i++) {
                        System.out.println(getName()+":"+i);
                    }
                    synchronized("A"){
                        for (int i = 50; i < 100; i++) {
                            System.out.println(getName()+":"+i);
                        }
                    }
                }
            }
        };
        t1.start();
        t2.start();
    }

上述代码的执行结果只能是执行到49,无法执行到后迷案的,因为出现了死锁。

避免死锁的方式:按照顺序加锁,或者设置一个超时等候,如果在一定范围内没有获取到锁,那么就不执行锁中的代码块。

public static void main(String[] args) {
        Thread t1=new Thread("小明"){
            public void run(){
               synchronized("A"){
                   for (int i = 0; i <50 ; i++) {
                       System.out.println(getName()+":"+i);
                   }
                   synchronized("B"){
                       for (int i = 50; i < 100; i++) {
                           System.out.println(getName()+":"+i);
                       }
                   }
               }
            }
        };

        Thread t2=new Thread("小红"){
            public void run(){
                synchronized("A"){
                    for (int i = 0; i <50 ; i++) {
                        System.out.println(getName()+":"+i);
                    }
                    synchronized("B"){
                        for (int i = 50; i < 100; i++) {
                            System.out.println(getName()+":"+i);
                        }
                    }
                }
            }
        };
        t1.start();
        t2.start();
    }

我们按照顺序给代码进行上锁,这样就不会出现死锁的情况了,因为都是同步代码,没有并行的代码。这样小明和小红都能执行到99.

4.6与锁相关的API

1.wait( )  释放已经占有的锁对象,进入到等待队列中,不参与时间片的争抢,也不参与锁的争抢,需要等待其他线程调用通知方法。

有两个重载方法 wait(long timeout ): 指定一个等待的时间,超过这个时间会被自动唤醒

wait(long timeout , int nanos):指定等待的时间更为精确。

2.notify( )  通知,唤醒等待队列的某一个线程,被唤醒的线程是随机的,开始参与锁对象的争抢

3.notifyAll( )  通知,唤醒等待队列中的所有线程,都开始争抢锁对象。

我们可以通过上述的下载图片的案例来更好的理解这些方法:

 public static void main(String[] args) {
        //定义一个对象,充当锁
        Object lock = new Object();
        //定义一个下载线程
        Thread down = new Thread(()->{

                System.out.println("---开始下载图片---");
                for (int i = 0; i < 11; i++) {
                    System.out.println(Thread.currentThread().getName()+"百分比:"+i*10+"%");
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("---图片下载完成---");
            synchronized (lock) {
                //通知等待队列中的某一个线程
                //如果想要调用锁的通知方法,那么也必须先获取锁对象
                lock.notifyAll();
            }
        },"下载图片");

首先我们在main方法中定义了一个锁对象,之后开始定义下载线程,使用for循环,并设置一个休眠时间是200ms,在循环结束之后打印图片下载完成,之后通过上锁的方式通知所有线程开始抢锁

//定义一个显示线程
        Thread show = new Thread(()->{

                //该显示线程,纲要执行任务时,就应该进入等待队列,等待下载队列
                try {
                    synchronized (lock) {
                        //如果想要进入等待队列,必须先获取锁对象,然后在调用锁对象的wait方法
                        lock.wait();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //当下载线程结束后,该线程得到通知了,然后才有机会获取锁对象,然后获取时间片段,才能继续向下执行
                System.out.println("---开始显示图片---");
                for (int i = 0; i < 11; i++) {
                    System.out.println(Thread.currentThread().getName()+"百分比:"+i*10+"%");
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("---图片显示完成---");

        },"显示图片");
        down.start();
        show.start();

之后在显示线程中,我们需要先对线程进行休眠操作,让下载线程先执行,当下载结束之后,再执行显示线程。

4.7  ReentrantLock

4.7.1 可重入锁的简介

ReentrantLock指的是可以让一个线程多次获取锁的类型;里面内置了一个计数器用于记录当前线程获取了该锁的次数,可以有效地避免出现死锁的情况。

该类提供了两个子类型,非公平锁和公平锁,非公平锁执行的顺序不是按照线程的请求顺序,而是可能发生插队现象,这种锁执行效率更高,但是可能会增加线程的饥饿情况;

公平锁:多个线程获取锁的方式是按照线程的请求顺序,谁都能获取到,减少了线程的饥饿情况,但是会降低系统的吞吐量,在构造器种传入true则表示公平锁,传入false则表示非公平锁。

  该类比使用synchronized关键字更加灵活,但是需要手动上锁或者解锁,

lock(): 上锁方法 锁对象没有被其他线程占用时,就会上锁成功,否则当前线程处于阻塞状态。unlock(): 解锁方法,必须在占有锁的时候,才能进行解锁,否则报异常;

tryLock(): 尝试获取锁,如果获取不到,并不阻塞,而是执行其他代码 

                获取不到锁返回false,获取到锁返回true

tryLock(long time , TimeUnit unit):  

可以指定一定时间内获取锁对象,如果超过这个时间还没有获取到锁对象,就返回false,

指定时间内获取到锁,就返回true

class MyCounter implements Runnable {
    private  int count = 0;
    private  String name;
    ReentrantLock lock = new ReentrantLock(true);
    public MyCounter(String name) {
        this.name = name;
    }
    public void run() {
        //先创建锁对象,构造器中,可以传入true或者false,来表示公平和非公平,默认就是非公平
        //调用lock方法,进行上锁
        lock.lock();
            for (int i = 0; i < 10; i++) {
                count++;
                System.out.println(Thread.currentThread().getName() + " 使用了秒表进行计数 "+count);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        //同步代码块执行完后,需要解锁,给别的线程获取锁的机会
        lock.unlock();

    }

我们首先定义一个MyCounter类,提供私有的成员变量;之后创建一个可重入锁对象lock,是一个公平锁,重写run任务方法,调用锁对象对需要同步的代码块进行上锁,代码执行完后,需要调用锁对象解锁。

public static void main(String[] args) {
        //使用两个线程来模拟两个人来使用计数器
        MyCounter counter = new MyCounter("秒表");
        Thread t1 = new Thread(counter,"小明");
        Thread t2 = new Thread(counter,"小红");
        t1.start();
        t2.start();
    }

在main方法中,我们创建了一个该类的对象,并实例化两个线程,先启动小明,再启动小红。

4.7.2  可重入锁的标准写法

把释放锁的操作写在finally模块中

class MyCounter1 implements Runnable {
    private  int count = 0;
    private  String name;
    ReentrantLock lock = new ReentrantLock(true);
    public MyCounter1(String name) {
        this.name = name;
    }
    public void run() {
        //先创建锁对象,构造器中,可以传入true或者false,来表示公平和非公平,默认就是非公平

        //调用lock方法,进行上锁
        lock.lock();
        for (int i = 0; i < 10; i++) {
            count++;
            System.out.println(Thread.currentThread().getName() + " 使用了秒表进行计数 "+count);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        /**
         * 当在释放锁之前,发生了异常,那么锁就无法释放了,那么别的线程想要获取锁对象,变成了不可能,所以都会处于阻塞状态。
         *
         * 因此为了不阻塞其他线程对于锁的获取,当前线程不管是否有无异常,那都应该正确的释放锁
         * 所以锁的释放应该放在try的finally模块里
         */
        try {
            String str = null;
            System.out.println(str.length());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        //同步代码块执行完后,需要解锁,给别的线程获取锁的机会

再释放锁之前,如果代码片段发生了异常,那么此时锁就会无法释放,别的线程就不可能获取到锁对象,我们需要将解锁的方法放入到finally中实现.

public class TicketCenterDemo {
    public static void main(String[] args) {
        TicketCenter ticket = new TicketCenter(100);
        Thread t1 = new Thread(()->{
                    try {
                        ticket.buyOne();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                },"小明"
        );
        Thread t2 = new Thread(()->{
            try {
                ticket.buyOne();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        },"小花");

        Thread t3 = new Thread(()->{
            try {
                ticket.buyBatch(20);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        },"小强");

        Thread t4 = new Thread(()->{
            try {
                ticket.buyBatch(20);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        },"小丽");

        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }
}

/**
 * 购票中心
 */
class TicketCenter extends Thread{
    public static ReentrantLock lock = new ReentrantLock();
    int ticket ;
    public TicketCenter(int ticket) {
        this.ticket = ticket;
    }
    //购买一张高铁票
    public void buyOne(){
        boolean success = false;
        try {
            success = lock.tryLock(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        if(success){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName()+"开始买票,剩余"+ticket);
                try {
                    Thread.sleep(1000);
                    ticket--;
                    System.out.println(Thread.currentThread().getName()+"买完票了,剩余"+ticket);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }finally {
                    lock.unlock();
                }
            }
        }else{
            System.out.println(Thread.currentThread().getName()+"不等了,转身就走了");
        }
    }

    //黄牛买多张票
    public void buyBatch(int num){
        boolean success ;
        try {
            success = lock.tryLock(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        if(success){
            if(ticket > 0&&num < ticket){
                System.out.println(Thread.currentThread().getName()+"开始买票,剩余"+ticket);
                try {
                    Thread.sleep(10000);
                    ticket-=num;
                    System.out.println(Thread.currentThread().getName()+"买完票了,剩余"+ticket);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }finally {
                    lock.unlock();
                }
            }
        }else{
            System.out.println(Thread.currentThread().getName()+"不等了,转身就走了");
        }
    }

}

上述代码是tryLock的案例演示,有助于增加理解.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值