java多线程,超详细总结,看这一篇文章就够了(未完,待续...)

java多线程入门

一、进程和线程

什么是进程?

电脑中会有很多单独运行的程序,每个程序有一个独立的进程,而进程之间是相互独立存在的。如下图中的360安全卫士

在这里插入图片描述

什么是线程

​ 进程想要执行任务需要依赖线程。换句话说,就是进程中的最小执行单位就是线程。并且一个进程至少有一个线程

​ 提到多线程就有两个概念,就是串行和并行。

​ 所谓串行,其实是相对于单线程来执行多个任务来说的,举个例子:当我们下载多个文件时,在串行中它是按照一定的顺序去进行下载的,也就是说,必须等下载完A之后才能开始下载B,它们在时间上是不能能发生重叠的。

在这里插入图片描述

​ 并行:下载多个文件,开启多线程,多个我呢见同时进行下载,这里是严格意义上的,在同一时刻发生的,并行在时间上是重叠的

在这里插入图片描述

进程与线程的联系

  • ​ 一个进程最少拥有一个线程——主线程——运行起来就执行的线程
  • ​ 线程之间是共享内存资源的(这个内存资源由进程申请的)
  • ​ 线程之间可以通信(进行数据传递:多为主线程和子线程)

什么是上下文切换

​ 即使是单核CPU也支持多线程,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配分配给各个线程的时间,因为时间片非常短,所以CPU通过不停的切换线程执行,让我们感觉多个线程时同时执行,时间片一般时几十毫秒。

​ CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换(很耗资源)。

​ 在计算机中,内核切换的过程中有两种状态,一种状态叫用户态,一种状态叫内核态,只有kenel能调度相关的进程。

二、实现线程的几种方式

实现多线程有两种方式:(自JDK1.5之后有三种,最后一种不常用)

1、继承Thread类实现run方法
步骤:
  • ​ 定义类继承Thread
  • ​ 重写Thread类中的run方法(目的:将自定义代码存储在run党法,让线程运行)
  • ​ 调用线程的start方法:(该方法有两步:启动线程,调用run方法)
package com.xinzhi.build;

/**
 * @author 一只因特码
 * @data 2020/7/7 18:01
 */
public class UserThread {
    static class MyTask extends Thread{
        public static void main(String[] args) {
            System.out.println(1);
            System.out.println(2);
            new MyTask().start();
            System.out.println(4);
            try {
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(5);
        }
        @Override
        public void run(){
           // System.out.println("这是通过继承实现的多线程!!!");
            System.out.println(3);
        }
    }
}

2、实现Runnable接口

​ 注意事项: 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run 的无参 方法。

package com.xinzhi.build;

/**
 * @author 一只因特码
 * @data 2020/7/7 18:22
 */
public class UserRunnable {
    public static void main(String[] args) {
//        System.out.println(1);
        new Thread(new Task()).start();
        System.out.println(2);

        System.out.println(1);

        //使用匿名内部类
//        new Thread(new Runnable() {
//            public void run() {
//                System.out.println(3);
//            }
//        }).start();
        //使用lambda表达式
        new Thread(()-> System.out.println(3)).start();
        System.out.println(2);
    }


//    static class Task implements Runnable{
//
//        public void run() {
//            System.out.println(3);
//        }
//    }
}

步骤:
  • ​ 创建任务: 创建类实现Runnable接口
  • ​ 使用Thread 为这个任务分配线程
  •   开启任 务 Start() 
    

​ 一个类如果实现了Runnable接口或者继承了Thread类,那么它就是一个多线程类,,如果是要实现多线程,还需要重写run()方法,所以run() 方法是多线程的入口。

有返回值的线程

实现Callable接口,Callable实现call方法

package com.xinzhi.build;

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

/**
 * @author 一只因特码
 * @data 2020/7/7 18:49
 */
public class UserCallable {
    public static void main(String[] args) throws Exception {
        System.out.println(2);
        FutureTask<Integer> futureTask=new FutureTask<>(new Task());
        System.out.println(3);
        new Thread(futureTask).start();
        System.out.println(4);
        //futureTask.get()是一个阻塞的方法,直到线程将方法调用完毕只能拿到对应得值
        int result=futureTask.get();

        System.out.println(5);
        System.out.println(result);
        System.out.println(6);
    }
    static class  Task implements Callable<Integer>{

        @Override
        public Integer call() throws Exception {
            ThreadUtils.sleep(200);
            return 1;
        }
    }
}

守护线程

守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。所以java里线程分2种,

1、 守护线程,专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,那么jvm就会退出(即停止运行)——此时,连jvm都停止运行了,守护线程当然也就停止执行了。

​ 再换一种说法,如果有用户自定义线程存在的话,jvm就不会退出——此时,守护线程也不能退出,也就是它还要运行,干嘛呢,就是为了执行垃圾回收的任务啊。

2、用户自定义线程

​ 应用程序里的线程,一般都是用户自定义线程。

注意:

​ setDaemon(true)必须在调用线程的start()方法之前设置,否则会抛出IllegalThreadStateException异常。

package com.xinzhi.build;

import javax.swing.plaf.synth.SynthRadioButtonMenuItemUI;

/**
 * @author 一只因特码
 * @data 2020/7/7 19:17
 */
public class Deamon {
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            int count=10;
//            Thread t2 =new Thread(()->{
//                while (true){
//                    ThreadUtils.sleep(300);
//                    System.out.println("我是个守护线程");
//                }
//            });
//            t2.setDaemon(true);
//            t2.start();
            while (count>=0){
                ThreadUtils.sleep(200);
                System.out.println("我是用户线程");
                count--;
            }
            System.out.println("用户线程结束——————————————————-");

        });
        //守护的是主线程main方法
        t1.setDaemon(true);
        t1.start();
    }
}

解决线程安全

举例说明

package com.xinzhi.ticket;

/**
 * @author一只因特码
 * @data 2020/7/7 20:04
 */
public class Ticket implements Runnable {
    String name;
    public Ticket (String name){
        this.name=name;
    }
    @Override
    public void run() {
        while (com.xinzhi.ticket.Constant.count > 0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name+"出票一张,还剩"+ Constant.count--+"张");
        }
    }

    public static void main(String[] args) throws Exception {
        Thread one=new Thread(new Ticket("一号窗口"));
        Thread two=new Thread(new Ticket("二号窗口"));
        one.start();
        two.start();
        Thread.sleep(1000);
    }
}

package com.xinzhi.ticket;

/**
 * @author 一只因特码
 * @data 2020/7/7 20:12
 */
public class Constant {
    public static int count=100;
}

得到的结果

在这里插入图片描述

两种解决方式

同步代码块的第一种方式—synchronized(参数就是一个监听器)

synchronized(监听器/对象/对象的一把锁){ //需要同步的代码 }
package com.xinzhi.ticket;

import com.xinzhi.build.ThreadUtils;
import org.omg.CORBA.PUBLIC_MEMBER;

/**
 * @author 一只因特码
 * @data 2020/7/7 20:22
 */
public class Ticket1 implements Runnable {
    private static  final  Object monitor =new Object();
    String name;
    public Ticket1(String name){
        this.name=name;
    }
    @Override
    public void run() {
        while(Constant.count>0){
            ThreadUtils.sleep(100);
            //内置锁
            synchronized (Ticket1.monitor){
                System.out.println(name+"出票一张,还剩"+Constant.count--+"张");
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Thread one=new Thread(new Ticket("一号窗口"));
        Thread two=new Thread(new Ticket("二号窗口"));
        one.start();
        two.start();
        Thread.sleep(1000);
    }
}

同步代码块的第二种方式—ReentrantLock

package com.xinzhi.ticket;

import com.xinzhi.build.ThreadUtils;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 一只因特码
 * @data 2020/7/7 20:47
 */
public class Ticket2 implements Runnable {
    //ReentrantLock可重入锁,显示锁
   private static ReentrantLock lock=new ReentrantLock();
   String name;
   public Ticket2(String name){
       this.name=name;
   }
    @Override
    public void run() {
        while (Constant.count>0){
            ThreadUtils.sleep(100);
            lock.lock();
            System.out.println(name+"出票一张,还剩"+Constant.count+"张");
            lock.unlock();
        }
    }

    public static void main(String[] args) throws Exception {
        Thread one=new Thread(new Ticket("一号窗口"));
        Thread two=new Thread(new Ticket("二号窗口"));
        one.start();
        two.start();
        Thread.sleep(1000);
    }
}

多线程实现同步的方法

同步的实现方面有两种,分别是synchronized,wait与notify

wait():使一个线程处于等待状态,并且释放所持有的对象的lock。

sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。

notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。

Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

三、java中的锁

参考链接 https://blog.csdn.net/qq_41181619/article/details/81407289

一、在java中的锁分为以下:
1、公平锁/非公平锁

​ 公平锁是指多个线程按照申请锁的顺序来获取锁。
​ 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

2、可重入锁

​ 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁

3、独享锁/共享锁

​ 独享锁是指该锁一次只能被一个线程所持有。
​ 共享锁是指该锁可被多个线程所持有。

4、互斥锁/读写锁

​ 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现

5、乐观锁/悲观锁

​ 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
​ 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

​ 重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁

6、分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

7、偏向锁/轻量级锁/重量级锁

​ 这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

偏向锁的适用场景

始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;
在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

8、自旋锁(java.util.concurrent包下的几乎都是利用锁)

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗

自旋锁尽可能的减少线程的阻塞,适用于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗

但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu的线程又不能获取到cpu,造成cpu的浪费。

从底层角度看常见的锁也就两种:Synchronized和Lock接口以及ReadWriteLock接口(读写锁)

Synchronized是基于JVM来保证数据同步的,而Lock则是在硬件层面,依赖特殊的CPU指令实现数据同步的

  • ​ Synchronized,它就是一个:非公平,悲观,独享,互斥,可重入的重量级锁
  • ​ ReentrantLock,它是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。
  • ​ ReentrantReadWriteLocK,它是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。

二、重量级锁Synchronized

1、Synchronized的作用

在JDK1.5之前都是使用synchronized关键字保证同步的,它可以把任意一个非NULL的对象当作锁。

  1. 作用于方法时,锁住的是对象的实例(this);

  2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;

  3. synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。

2、synchronized原理分析

Java对象头和monitor是实现synchronized的基础!

对象在内存中的布局

在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据 (Instance Data)、对齐填充(Padding)。一般而言,synchronized使用的锁对象是存储在Java对象头 里。它是轻量级锁和偏向锁的关键。

对象头
  • HotSpot虚拟机的对象头包括用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代 年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64 位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”
  • class类型指针,指向这个对象属于哪个Class对象
  • 数组长度(只有数组对象有) 如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组 长度
实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从

父类继承下来的,还是在子类中定义的,都需要记录起来。

对齐填充

于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

3、synchronized 锁的升级

​ 在 Java 语言中,使用 Synchronized 是能够实现线程同步的,即加锁。并且实现的是悲观锁,在操作同 步资源的时候直接先加锁。(锁可以升级,但不能降级)

​ 在 jdk6 之后便引入了“偏向锁”和 “轻量级锁”,所以总共有4种锁状态,级别由低到高依次为:无锁状态、偏向锁状态、轻量级锁状态、重 量级锁状态。

第一步:偏向锁

偏向锁的作用是当有线程访问同步代码或方法时,线程只需要判断对象头的Mark Word中判断一下是否 有偏向锁指向线程ID.

偏向锁记录过程

  • 线程抢到了对象的同步锁(锁标志为01参考上图即无其他线程占用)
  • 对象Mark World 将是否偏向标志位设置为1
  • 记录抢到锁的线程ID
  • 进入偏向状态
什么时候升级成轻量级锁?

偏向锁 -> 轻量级锁 -> 重量级锁

一旦出现其他线程竞争资源时,偏向锁就会被撤销。

偏向锁的插销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果 是,则升级锁。反之则其他线程抢占

第二步:轻量级锁

当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会尝试获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。

第一个前来获取:记录了偏向的线程

第二个过来尝试获取,如果成功了,说明第一个线程已经不再使用,锁则偏向第二个线程 如果失败,说明存在竞争,升级轻量级锁

第三步:自旋锁

JVM 提供了一种自旋锁,可以通过自旋方式不断尝试获取锁,从而避免线程被挂起阻塞。这是基于大多 数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。

从 JDK1.7 开始,自旋锁默认启用,自旋次数由 JVM 设置决定,这里我不建议设置的重试次数过多,因 为 CAS 重试操作意味着长时间地占用 CPU。自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重 量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。默认的自旋次数是十次

第四步:重量级锁

自旋失败,很大概率 再一次自选也是失败,因此直接升级成重量级锁,进行线程阻塞,减少cpu消耗。

当锁升级为重量级锁后,未抢到锁的线程都会被阻塞,进入阻塞队列。

4、Lock
获取锁的两种方式:
Lock lock = ...; 
lock.lock();
try{//处理任务 
}catch(Exception ex){
}finally{ lock.unlock(); //释放锁 
        }
Lock lock = ...; if(lock.tryLock()) { 
try{
//处理任务
}catch(Exception ex){ 
}finally{ 
lock.unlock(); //释放锁 
} 
}else { //如果不能获取锁,则直接做其他事情
}
Lock的实现类 ReentrantLock

ReentrantLock,即 可重入锁。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock 提供了更多的方法

5、 ReadWriteLock (读写锁)
武三水:
package com.xinzhi.build;

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

/**
 * @author 一只因特码
 * @date 2020/7/7 21:32
 */
public class ReadAndWriteLockTest {
    public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {

        //同时读、写
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new Runnable() {
            @Override
            public void run() {
                readFile(Thread.currentThread());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                writeFile(Thread.currentThread());
            }
        });
    }

    // 读操作
    public static void readFile(Thread thread) {
        lock.readLock().lock();
        boolean readLock = lock.isWriteLocked();
        if (!readLock) {
            System.out.println("当前为读锁!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在进行读操作……");
            }
            System.out.println(thread.getName() + ":读操作完毕!");
        } finally {
            System.out.println("释放读锁!");
            lock.readLock().unlock();
        }
    }

    // 写操作
    public static void writeFile(Thread thread) {
        lock.writeLock().lock();
        boolean writeLock = lock.isWriteLocked();
        if (writeLock) {
            System.out.println("当前为写锁!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在进行写操作……");
            }
  System.out.println(thread.getName() + ":写操作完毕!");
        } finally {
            System.out.println("释放写锁!");
            lock.writeLock().unlock();
        }
    }
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值