Java学习笔记 11、快速入门多线程(详细)(一)

简介: Java学习笔记 11、快速入门多线程(详细)(一)

一、多线程基本认识


1、程序、进程、线程


程序(program):指代完成指定任务并使用某种语言编写的一组指令的集合,也指代一段静态的代码。


进程(process):程序的一次执行过程,也可以用一个正在运行的程序来表示进程。它有自己的一个生命周期,自身产生、存在与消亡的过程。


线程(thread):进程中可以细化为线程,我们平时使用的就是主线程,我们也可以开辟其他线程来帮我们并行做其他事。若一个进程同一时间并行执行多个线程,就是支持多线程的!


线程作为调度和执行的单位,每个线程都有自己独立的运行栈和程序计数器(pc),并且线程切换的开销小。

一个进程中的多个线程共享相同的内存单元/内存地址空间,从同一堆中分配对象,可以访问相同的变量和对象,但多个线程共享的系统资源可能就会带来安全的隐患。


单线程与多线程见图:




2、认识单核与多核CPU


单核CPU:其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。好比高度公路有多个车道但是只有一个工作人员收费。由于CPU时间单元短,我们平时运行程序时也不会感觉出来。


多核CPU:能够更好的发挥多线程的效率,现在我们一般电脑都是多核的,服务器也是。


例如我的电脑是8核的,对于暂时学习做一些普通的项目都是够得:此电脑-右击管理—设备管理器即可查看


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vyr2szlY-1612970769700)(C:\Users\93997\AppData\Roaming\Typora\typora-user-images\image-20210204215155208.png)]


补充知识点:一个Java应用程序例如java.exe,其实最少有三个线程如main()主线程,gc()垃圾回收线程,异常处理线程。一旦发生异常的话就会影响主线程。



并行:多个CPU同时执行多个任务,例如多个人同时做不同的事情。


并发:一个CPU(采用时间片)同时执行多个任务。比如秒杀项目,多个人做同一件事。



3、多线程优点


背景介绍:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用时更短,但为什么仍需要使用多线程呢?


单核时,采用多线程会比采用单线程会更慢,因为进行多线程的过程中需要来回不断切换线程。

多核时,采用多线程就会比采用单线程快了,此时不需要来回进行切换。

多线程优点:


提高应用程序的响应,尤其是在图形化界面中更会使用到,增强用户的体验。

提高计算机系统CPU的利用率。

改善程序结构。将既长又复杂的进程分为多个线程,独立运行,方便于理解与修改。

何时需要使用多线程:


程序需要同时执行两个或多个任务。

程序需要实现一些需要等待的任务时,例如用户输入、文件读写操作、网络操作、搜索等。

需要后台运行的程序。


4、一个以上的执行空间说明


《head first java 2.0》中的一个问题:有一个以上的执行空间代表什么?


     当我们创建多个线程,有超过一个以上的执行空间时,看起来会像是有好几件事情同时发生。实际上,只有真正的多处理器才能够同时执行多件事情(前面也提到了)。


     对于使用在Java中的线程可以让它看起来好像同时都在执行中,实际上执行动作在执行空间中非常快速的来回交换,所以我们会有错觉每项任务都在同时进行,这里说个数字在100个毫秒内目前执行程序代码会切换到不同空间上的不同方法。


这里又有一个问题:Java在操作系统中也只是个在底层操作系统上执行的进程。一旦轮到Java执行时,Java虚拟机会执行什么?


目前执行空间最上面的会执行。


书中的截图:描述线程执行空间




二、线程的创建与使用


认识Thread类

Java的JVM允许程序运行多个线程,jdk也提供了相应的API,Thread类


Thread类:


我们想让一个线程做指定的事情是要通过某个特定Thread对象的run()方法来完成操作的,将run()方法的主体成为线程体。

想让run()中的内容执行并不是直接调用run()方法,而是调用其Start()方法。

为啥不调用run()方法呢?因为run()方法是我们进行重写的,若是直接调用run()方法来启动会当做普通方法调用的,那么就是主线程来执行了;调用start()方法启动线程,整个线程处于就绪状态,等待虚拟机调度,执行run方法,一旦run方法结束,此线程终止。

Thread类的构造器:


Thread():创建新的Thread对象

Thread(String threadname):创建线程并指定线程实例名

Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接 口中的run方法

Thread(Runnable target, String name):创建新的Thread对象

常用方法:


void start():启动线程,并执行对象的run()方法。

String getName():返回线程的名称。

void setName():设置该线程名称。

static native Thread currentThread():静态⽅法,返回对当前正在执⾏的线程对象的引⽤。

static native void yield():表示放弃的意思,表示当前线程愿意让出对当前处理器的占用,需要注意就算当前线程调用其方法,程序在调度时,也还是可能会执行该线程的。

static native void sleep(long millis):指定millis毫秒数让当前线程进行睡眠,睡眠的状态是阻塞状态。

final void join():在线程a中可以调用线程b的join()方法,此时线程a进入阻塞状态,知道线程b执行完之后,a才结束阻塞状态。内部调用的是Object的wait()方法。



两种创建线程方式


1、创建线程方式一:继承Thread


为什么要继承Thread类呢?我们想要让其他线程执行自己的事情那么就需要先继承Thread类并重写run()方法,这样我们创建自定义线程类实例,调用start()方法即可。


这个run()方法其实是Thread类实现Runnable接口里的方法。

继承Thread方式:


class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0;i<100;i++){
            System.out.println(i);
        }
    }
}
public class Main {
    //本身main方法是主线程
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();//①虚拟机中创建一个线程  ②调用线程的run()方法
        //主线程下执行语句
        for(int i=0;i<50;i++)
            System.out.println("主线程中循环:"+i);
    }
}



可以看到主线程与自己创建的线程进行交互执行任务

注意点:


在程序中两个线程在不断的切换执行,他们的打印顺序不会一样,run()方法由JVM调用,什么时候调用执行过程控制都有操作系统的CPU调度(CPU调度策略,主要执行进程的顺序)决定。

记住要使用run()方法需要调用start()方法,若是调用run(),那么就只是普通方法,并没有启动多线程。

一个线程对象只能调用一次start()方法启动,如果重复调用了,会抛异常IllegalThreadStateException。当调用了start()后,虚拟机jvm会为我们创建一个线程,然后等这个线程第一次得到时间片再调用run()方法。


还可以使用Thread的匿名实现类来创建线程:


//本身main方法是主线程
public static void main(String[] args) {
    //使用Java8的函数式编程
    new Thread(()->{
        System.out.println("Thread的匿名实现类");
    }).start();
    //普通重写方法
    new Thread(){
        @Override
        public void run() {
            System.out.println("匿名实现类");
        }
    }.start();
}


2、创建线程方式二:实现Runnable接口


通过实现Runnable接口方式来创建对象,实现该接口的类作为参数传递到构造器中:


class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("实现Runnable接口");
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        //方式一:通过实现接口类,并创建实例作为参数放置到Thread类中
        Thread thread = new Thread(new MyRunnable());
        thread.start();
        //方式二:通过给Thread类传入匿名接口类
        new Thread(new MyRunnable(){
            @Override
            public void run() {
                System.out.println("匿名接口类runnable");
            }
        }).start();
    }
}


这里演示了两种使用实现Runnable接口方式来创建线程


源码解析:对于非继承方式实现Runnable接口在进行start()时进行调用作为参数中的run()方法


//构造器传入接口实现类,采用多态方式,调用了init()方法将target传入
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
//init()方法
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    ....
}
//当调用start()方法实际上也会调用run()方法,看一下实际上就是调用的target.run()方法也就是之前传入的target
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}




对于实现Runnable接口来创建多个线程方式:


class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("实现Runnable接口");
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        //若是要多线程调用同一个接口实现类的run()方法,就需要创建多个Thread实例
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}



比较两种创建方式

对于线程中共享的属性声明介绍:


继承Thread类实现的,想要共享其中的属性,那么需要使用static来进行修饰

实现runnable接口的,只要是new Thread(runnable)中的runnable实现类是同一个时,其中属性默认就是共享的。

那么哪个方式我们更常使用呢?


开发中优先选择实现runnable接口的方式


实现方式没有类的单继承局限性。

实现方式更适合来处理多个线程有共享数据的情况。

降低了线程对象和线程任务的耦合性。


常用方法

修改线程名


//方式一:使用setName(threadname)方法更改,需要创建实例之后
new Thread(){
    ...run()
}.setName("线程一");
//方式二:创建实例时,使用有参构造器修改线程名 Thread(String name)
new Thread("线程一")
//方式三:双参构造(实现runnable接口情况下)
new Thread(myRunnable, "线程1");


修改了线程名,我们总要输出线程名吧:


class MyThread extends Thread{
    @Override
    public void run() {
        //方式一:在线程中直接调用getName()
        System.out.println(this.getName());
    }
}
public class Main {
    public static void main(String[] args) {
        new MyThread().start();
        //方式二:通过Thread的静态方法currentThread()获取当前线程实例,调用方法即可
        System.out.println(Thread.currentThread().getName());
    }
}



yield()方法
class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if(i%4==0){
                //使用该方法会交出当前cpu的占用,让其他线程执行
                yield();
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
public class Main {
    public static void main(String[] args) {
        new MyThread().start();
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}



这里测试的是当i%4==0时让出占用的cpu资源,那么就会执行其他线程了



注意:使用yield()方法并不是每一次都有效的,也有可能继续执行当前线程,理解含义即可。



join()方法
class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread mythread = new MyThread();
        mythread.start();
        for (int i = 0; i < 10; i++) {
            if(i == 5){
                try{
                    //此时主线程阻塞,开始执行b线程,b线程执行完以后,主线程阻塞结束
                    mythread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}


当i==5时,我们让主线程阻塞,来执行指定线程中内容,当指定线程执行完,主线程阻塞结束继续执行



sleep()方法
class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            if(i%2 == 1){
                try {
                    //让该线程睡眠(阻塞)1秒
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread mythread = new MyThread();
        mythread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}


这里是当i%2==0时该线程进入睡眠(阻塞)1秒。



注意点:sleep()是Thread类中静态方法,在哪个线程中调用该方法哪个线程就会进入睡眠,就算你在a线程中调用b线程的sleep()也还是a线程进入睡眠。



线程优先级设置

介绍调度

CPU通过为每个线程分配CPU时间⽚来实现多线程机制,对于操作系统分配时间片给每个线程的涉及到线程的调度策略。



抢占式:高优先级的线程抢占CPU

针对于Java的调度方法:


同优先级线程组成先进先出队列(先到先服务),使用时间片策略

对于高优先级,使用优先调度的抢占式策略


线程优先级

Java中线程的优先级等级如下,定义在Thread类中的静态属性:


MAX_PRIORITY:10

MIN _PRIORITY:1

NORM_PRIORITY:5

设置与获取当前优先级方法:


setPriority(int newPriority) :改变线程的优先级
getPriority() :返回线程优先值
//优先值可以直接使用Thread类中的静态变量
myThread.setProority(Thread.MAX_PRIORITY);


对于优先级的说明:


线程创建时继承父线程的优先级

低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才调用。


线程的分类

Java中的线程分为两类:守护线程与用户线程


用户线程:我们平常创建的普通线程。

守护线程:用来服务于用户线程,不需要上层逻辑介入。当线程只剩下守护线程时,JVM就会退出,若还有其他用户线程在,JVM就不会退出。

这两种线程几乎是一样的,唯一的区别是判断JVM何时离开!


如何设置线程为守护线程呢?


在调用start()方法前,调用Thread.setDaemon(true)。就可以把一个用户线程变成一个守护线程。

java垃圾回收就是一个典型的守护线程,若是jvm中都是守护线程时,JVM就会退出。


守护线程应用场景:


在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用,免去了有些时候主线程结束子线程还在执行的情况,当jvm只剩下守护进程时,JVM就会退出。

相对于用户线程,通常都是些关键的事务,这些操作不能中断所以就不能使用守护线程。


三、线程的生命周期


Thread.State中的六种状态

线程也具有生命周期,在JDK中使用Thread.State类来定义线程的几种状态,下图是六种状态:



NEW(初始):新创建的一个线程对象,还没有调用start()。

RUNNABLE(运行):Java线程中将就绪(ready)和运行中(running)两种状态笼统称为"运行"。若调用了satrt()方法,该状态线程位于可运行线程池中,等待被线程调度选中,获取CPU使用权限,此时处于就绪状态(ready)。就绪状态线程获得CPU时间片后变为运行中状态(running)。

BLOCKED(阻塞):线程处于阻塞状态,等待监视锁。

WAITING(等待):在该状态下的线程需要等待其他线程做出一些特定动作(通知或中断)。

TIMED_WAITING(超时等待):调用sleep()、join()、wait()方法可能导致线程处于等待状态,不同于WAITING,它可以在指定时间后自行返回。

TERMINATED(终止):表示线程执行完毕。

参考文章:Java线程的6种状态及切换(透彻讲解)


详细说明:


初始状态(NEW)

实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

就绪状态(RUNNABLE之READY)

就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。

调用线程的start()方法,此线程进入就绪状态。

当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。

当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。

锁池里的线程拿到对象锁后,进入就绪状态。

运行中状态(RUNNABLE之RUNNING)

线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。

阻塞状态(BLOCKED)

阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

等待(WAITING)

处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

超时等待(TIMED_WAITING)

处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

终止状态(TERMINATED)

当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。

在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。


生命周期中五种状态

Java语言使用Thread类及其实现类的对象来创建使用线程,完整的生命周期中通常要经历如下的五种状态:


新建—就绪—运行—阻塞—死亡


新建:当一个Thread类或其子类的对象被声明并被创建时,新生的线程对象处于新建状态。

就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。

运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。

阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态。

死亡:线程完成了它的全部工作(run方法结束)或线程被提前强制性地中止或出现异常导致结束。

周期图如下:



四、线程的同步


1、多窗口卖票(引出问题)


继承Thread与实现Runnable接口两种方式

多窗口卖票问题描述:我们在run()方法中模拟卖票的过程,一旦进入while第一条输出语句表示售出一张票,在多线程中若是进行就可能会出现下面的卖出重复票、错票的问题。


继承Thread的多窗口卖票问题


class MyThread extends Thread{
    //继承Thread方式想要共享属性:设置为static
    private static int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
    public MyThread(String name) {
        super(name);
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread1 = new MyThread("线程一");
        MyThread thread2 = new MyThread("线程二");
        thread1.start();
        thread2.start();
    }
}



使用继承Thread方式创建线程想要共享属性就需要设置static静态,因为都是new的同一个类的对象。



实现Runnable接口的多窗口卖票问题


class MyRunnable implements Runnable{
    //实现runnable接口方式想要共享属性:默认权限
    private int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        //使用相同的接口实现类runnable
        Thread thread1 = new Thread(runnable, "线程一");
        Thread thread2 = new Thread(runnable, "线程二");
        thread1.start();
        thread2.start();
    }
}


在实现runnable接口类中对于想要共享的属性不需要设置static,因为该类创建的实例都作为Thread构造器参数传入,使用的是一个runnable。



问题描述以及解决方案

问题:在卖票过程中,出现了重票、错票,出现了线程的安全问题。当某个线程操作车票的过程中,尚未操作完成时,其他操作与此同时参与进来执行导致共享数据的错误。


解决办法:对于多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中其他线程不可以参与执行。



2、同步机制(解决线程安全问题)


同步机制介绍

同步机制


同步机制:Java对于线程安全问题也出同步机制,当线程调用一个方法时在没有得到结果之前,其他线程无法参与执行。


有两种方式解决:


同步代码块:synchronized(同步监视器){ ...操作共享数据 },单独在方法中声明

同步方法:public synchronized void show (String name){ ,直接声明在方法上


synchronized介绍


synchronized锁是什么?


任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器)。

同步代码块:同步监视器可以是单一的对象,很多时候可以将this或类.class作为锁。

同步方法的锁:可以看到只需要在方法权限修饰前加入synchronized即可,它对应的锁是根据它的方法状态,若其方法是static静态方法(类.class);非静态方法(this)

synchronized充当角色如图:



同步锁机制:对于并发工作,你需要某种方式来防 止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须 锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。 —《Thinking in Java》


注意:


必须要确保使用同一个资源的多个线程共用一把锁,否则无法保证操纵共享资源的安全。

针对于同步方法中一个线程类中的所有静态方法都会共用同一把锁(类.class),非静态方法都会使用this充当锁;同步代码块一定要谨慎。


方式一:同步代码块

语法:


synchronized(同步监视器){ 
  //需要被同步的代码
}


共享数据:多个线程共同操作的变量,例如之前的ticket。

被同步代码:操作共享数据的代码。

同步监视器:俗称锁,可以是任意一个对象,都可以充当锁,多个线程必须共用一把锁。


解决之前两种方式进行多线程的线程安全问题:


继承Thread方式


class MyThread extends Thread{
    private static int ticket = 100;
    //这里单独创建一个静态对象
    private static Object object = new Object();
    @Override
    public void run() {
        while(true){
            //这里使用Object静态实例对象作为锁
            synchronized (object){
                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }
    public MyThread(String name) {
        super(name);
    }
}


这里的锁是使用的类中一个静态对象实例object,对于这种继承Thread方式进行多线程的话是要new多个MyThread类的,而Synchronized中的锁必须是指定单独一把锁,所以将该object作为锁。

实现runnable接口


//解决实现runnable接口的线程安全问题
class MyRunnable implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while(true){
            //同步代码块:this本身指的是runnable的实现类MyRunnable
            synchronized (this){
                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }
}


对于实现runnable方式的,我们可以直接使用this来充当锁,因为创建多个线程使用的是同一个Runnable。

注意:不管什么形式来创建多线程的,若是想要避免出现线程安全问题那么就要确保synchronized里的锁是一把锁也就是同一个对象。



方式二:同步方法

语法:


//非静态方法:锁为this
public synchronized void show(){
}
//静态方法:锁为类.class
public synchronized static void show(){
}


解决之前两种方式进行多线程的线程安全问题:


继承Thread方式


class MyThread extends Thread{
    //继承Thread方式想要共享属性:设置为static
    private static int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket<=0 || operate()){
                break;
            }
        }
    }
    //同步方法:静态方法 锁为MyThread.Class 单独一份
    public synchronized static boolean operate(){
        if(ticket > 0){
            System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
            ticket--;
            return ticket==0;
        }else{
            return true;
        }
    }
    public MyThread(String name) {
        super(name);
    }
}



这里就需要使用静态方法了,此时锁为MyThread.class单独一份,若是非静态的话就是this,而下面创建多线程就是多个实例,就会出现安全问题。

实现Runnable接口方式


class MyRunnable implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while(true){
    //一旦ticket<=0 马上退出
            if(ticket<=0 || operate()){
                break;
            }
        }
    }
    //非静态方法使用this来作为锁
    public synchronized boolean operate(){
        if(ticket > 0){
            System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
            ticket--;
            return ticket==0;
        }else{
            return true;
        }
    }
}



这里将判断操作单独抽成一个同步方法,这里是非静态的,锁就是this

注意:继承Thread方式中应使用静态方法来实现同步方法;实现runnable接口实现同步方法时,该方法是否为静态都可以,因为多个线程始终使用的是一个runnable,锁始终只有一份。



方式三:Lock锁

认识Lock锁


从JDK5.0开始,Java提供了更强大的线程同步机制—使用显示定义同步锁对象来实现同步,同步锁使用Lock对象充当。


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


如何使用Lock接口呢?


我们使用其Lock接口实现类ReentrantLock类(重进入锁)。

ReentrantLock类:它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock类,可以调lock()方法显式加锁,调unlock()方法释放锁。


实际使用:解决线程同步


解决之前两种方式进行多线程的同步问题:


解决继承Thread的同步问题


class MyThread extends Thread{
    //继承Thread方式想要共享属性:设置为static
    private static int ticket = 100;
    //静态实例:使用Lock锁
    private static Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while(true){
            //手动上锁
            lock.lock();
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
                ticket--;
            }else{
                lock.unlock();
                break;
            }
            //手动解锁
            lock.unlock();
        }
    }
}



针对于继承Thread方式的lock锁在创建实例时定义为static。之后再在操作共享数据外显示上锁与解锁。

解决实现Runnable接口同步问题


class MyRunnable implements Runnable{
    //实现runnable接口方式想要共享属性:默认权限
    private int ticket = 100;
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while(true){
            //上锁
            lock.lock();
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
                ticket--;
            }else{
                lock.unlock();
                break;
            }
            //解锁
            lock.unlock();
        }
    }
}



针对于实现Runnable的情况,我们就创建一个普通实例对象即可。多个线程使用的是同一个Runnable实现类。

注意:如果同步代码有异常,要将unlock()写入finally语句块。



3、同步方法的好处及坏处


好处:解决了线程的安全问题。


坏处(局限性):在操作同步代码时,只能有一个线程参与,其他线程需要等待开锁之后才能进入,相当于是一个单线程的过程,效率低。



4、同步的范围及释放与不释放锁的操作


同步范围


对于寻找代码是否存在线程安全问题几个关键点?


明确哪些方法是多线程运行的代码。

明确多个线程是否有共享数据。

明确多线程代码中是否有多条语句操作共享数据。

解决策略:对于多条操作共享数据的语句,只能让一个线程执行完之后再让下个线程执行,即所有操作共享数据的这些语句都要放在同步范围中。对于同步范围的大小也要有个度,范围小太的话往往可能会没有锁住所有安全的问题;范围太大的话没发挥多线程的功能。



释放锁与不会释放锁的操作


释放锁的操作:


当前线程的同步方法或同步代码块执行结束会自动释放锁。

当前线程在同步方法或同步代码块中遇到break、return终止了该代码块、该方法的执行。

当前线程在同步方法或同步代码块中出现了未处理的Error或Exception,导致异常结束。

当前线程在同步方法或同步代码块中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

不会释放锁的操作:


线程执行同步方法或同步代码块时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。

其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)

suspent()与resume()方法已弃用。


5、小练习


问题描述:银行有一个账户。 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打 印账户余额。


程序如下:


class Account{
    private double wallet;
    //将该方法设置为同步方法,锁为this指的是该Account类,这里是可以的
    public synchronized void deposit(double money){
        if(wallet>=0){
            wallet += money;
            System.out.println(Thread.currentThread().getName()+"向账户存储了"+money+"元,账户余额为:"+wallet);
        }
    }
}
class Customer extends Thread{
    private Account account;
    public Customer(Account account,String name) {
        super(name);
        this.account = account;
    }
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            account.deposit(1000);
        }
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Account account = new Account();
        new Customer(account,"用户A").start();
        new Customer(account,"用户B").start();
    }
}


这里直接将Account操控数据的方法设置为同步方法,不需要使用static,因为多个线程共同操作一个Account



相关文章
|
6天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
36 6
|
14天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
14天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
39 3
|
15天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
19天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
27 2
|
19天前
|
监控 Java 开发者
Java线程管理:守护线程与本地线程的深入剖析
在Java编程语言中,线程是程序执行的最小单元,它们可以并行执行以提高程序的效率和响应性。Java提供了两种特殊的线程类型:守护线程和本地线程。本文将深入探讨这两种线程的区别,并探讨它们在实际开发中的应用。
27 1
|
7月前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【2月更文挑战第22天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个主题,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。
64 0
|
7月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
|
4月前
|
存储 安全 Java
解锁Java并发编程奥秘:深入剖析Synchronized关键字的同步机制与实现原理,让多线程安全如磐石般稳固!
【8月更文挑战第4天】Java并发编程中,Synchronized关键字是确保多线程环境下数据一致性与线程安全的基础机制。它可通过修饰实例方法、静态方法或代码块来控制对共享资源的独占访问。Synchronized基于Java对象头中的监视器锁实现,通过MonitorEnter/MonitorExit指令管理锁的获取与释放。示例展示了如何使用Synchronized修饰方法以实现线程间的同步,避免数据竞争。掌握其原理对编写高效安全的多线程程序极为关键。
71 1
|
5月前
|
安全 Java 开发者
Java并发编程中的线程安全问题及解决方案探讨
在Java编程中,特别是在并发编程领域,线程安全问题是开发过程中常见且关键的挑战。本文将深入探讨Java中的线程安全性,分析常见的线程安全问题,并介绍相应的解决方案,帮助开发者更好地理解和应对并发环境下的挑战。【7月更文挑战第3天】
107 0
下一篇
DataWorks