首页 > 编程语言 > 详细

Java多线程

时间:2021-04-19 11:15:19      阅读:30      评论:0      收藏:0      [点我收藏+]

基础知识

  • 进程 和 线程

进程:正在运行的程序,是一个动态概念,是操作系统进行资源分配的基本单位。

线程:是操作系统进行调度的基本单位,即CPU分配时间的单位。线程也被称为轻量级进程

那么为什么要有线程?

进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;

而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。

  • 多线程实现方法

1、继承 Thread 类;2、实现 Runnable 接口 -- 不带返回值

//1、继承Thread类  重写run()   |  创建子类对象  通过对象调用start

//创建多个窗口进行售票
class Window extends Thread{
    private static int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}

Window t1 = new Window();
Window t2 = new Window();

t1.start();
t2.start();

依旧依赖Thread类,只是这次不能直接创建对象.start()

而是通过创建类,将其丢到Thread构造器中,得到新的对象.start()

//2、实现Runnable接口  实现run()  |  创建实现类  丢到Thread构造器中  调用start

class Window1 implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}

Window1 w = new Window1();

Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
t1.start();
t2.start();

两者比较:

开发中优先选择:实现Runnable接口的方式,Runnable接口出现更符合面向对象,将线程单独进行对象的封装。

原因:1、实现方式没有类的单继承性的局限性;2、实现的方式更适合处理多个线程有共享数据的情况。

关联之处:

public class Thread extends Object implements Runnable

可以看出Thread类也是实现Runnable接口

而方法2直接跳过Thread,直接实现Runnable接口,那么怎么行?自然还要求你创建子类,还需要丢到Thread中运行。

  • 线程状态
// Thread.State 源码
public enum State {
    NEW,   // 处于NEW状态的线程此时尚未启动
    RUNNABLE,  //当前线程正在运行中
    BLOCKED,  //阻塞状态,处于BLOCKED状态的线程正等待锁的释放以进入同步区
    WAITING,  //等待状态,处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒
    TIMED_WAITING,  // 超时等待状态,线程等待一个具体的时间,时间到后会被自动唤醒
    TERMINATED;  // 终止状态。此时线程已执行完毕。
}
技术分享图片
  • 产生死锁的必要条件

1、互斥 -- 某个资源每次只能被一个进程所使用

2、请求与保持 -- 一个进程自己持有资源的同时,还在申请别的资源

3、不可剥夺 -- 这些资源别的进程无法剥夺,只能等待资源持有者自己释放

4、循环等待 -- 若干进程首尾相接循环等待资源

  • sleep() 和 wait() 区别

两者都能暂停线程执行

不同点:

1、sleep()只是暂停执行,wait() 通常用于线程之间的交互/通信

2、wait()方法被调用后,线程不会自动苏醒(WAITING状态),需要别的线程调用同一个对象上的notify()进行唤醒;sleep()方法执行完成后,线程会自动苏醒(TIMED_WAITING状态)

3、sleep()不释放持有的锁,wait() 会释放

  • 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new 一个 Thread,线程进入了新建状态New。

调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。

start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

Synchronized关键字

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行

// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}

// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
//要是object改为this,就等价于第一种
//改为this.getClass()  || 类.class 就等价于第二种
public void blockLock() {
    Object o = new Object();
    synchronized (o) {
        // code
    }
}

注意:

如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象。

因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁,锁不一样,不满足互斥条件。

  • 利用Synchronized关键字 写一个死锁
public class DeadLock {
    static Object o1 = new Object();
    static Object o2 = new Object();
    public static void main(String[] args) {
        //实现类写法
        new Thread(new Runnable() {
            public void run() {
                synchronized (o1) {
                    System.out.println("线程1锁o1");
                    try {
                        Thread.sleep(1000);
                        synchronized (o2) {
                            System.out.println("线程1锁o2");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        }).start();
        
        //继承类写法
        new Thread(){
            @Override
            public void run() {
                synchronized (o2){
                    System.out.println("线程2锁o2");
                    synchronized (o1){
                        System.out.println("线程2锁o1");
                    }
                }
            }
        }.start();
    }
}

Synchronized和其他对比

  • synchronized 和 volatile 的区别?

Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,其中有final、synchronized和volatile关键字

final关键字修饰的变量是一个常量,所以在并发情况下不存在可见性问题

synchronized通过加锁的方式,比较完美的解决并发编程的三大问题。那么为什么还需要volatile的存在?我的理解主要是synchronized上锁机制还是太重了。

而volatile通过在volatile变量的操作前后插入内存屏障的方式,保证了变量在并发场景下的可见性和有序性,至于原子性可以通过atomic包来辅助完成。

内存屏障主要有两个作用:

1、阻止屏障两侧的指令重排序;

2、强制将缓冲区的脏数据写回主存,同时让缓存中的数据失效。

那么Synchronized是怎么具体解决这三个问题?

原子性:Synchronized关键字对应两个字节码指令:monitorenter 和 monitorexit,线程1在执行monitorenter指令时,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁。即使在执行过程中,CPU时间片用完,但是线程1还是持有锁,下次上CPU运行时,依旧只能线程1能拿到锁,这样就保证线程1能够完成的操作完整个代码块。

可见性:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。上锁前,需要清空变量值,重新从主存中加载最新值。

顺序性:synchronized无法禁止指令重排,但可以保证有序性?

上锁后,只有一个线程能进行操作,等同于单线程操作,单线程不必担心有序性问题。

  • synchronized 和 lock 的区别?

Synchronized是Java语言中的一个关键字。底层实现是基于JVM,它的锁可以是Java中一个对象,对于同步方法,锁是当前实例对象;对于静态同步方法,锁是当前对象的Class对象;对于同步方法块,锁是Synchonized括号里配置的对象。因为是JVM实现的,所以就避免了一些低级的编程错误,但是我们有时候还需要灵活的去应用锁,来达到性能的提升以及实现一些自定义的功能。

Lock是基于在语言层面实现的锁,ReentrantLock就是一个常用的Lock接口实现类,也就是基于JDK实现的,也就是说需要我们开发者手动的lock和unlock。

至于两者的区别,有好几种不同。

1、针对异常,synchronized会自动释放线程占有的锁,但lock如果没有在finally块中写unlock 就很有可能造成死锁。

2、针对中断,Lock可以让等待锁的线程响应中断,而synchronized却不行,它会让等待的线程会一直等待下去,不能够响应中断。

3、性能上,在资源竞争不激烈的情形下,两者差距不大;但是当同步非常激烈的时候,Lock的优势就能体现出来了。

  • 谈谈 synchronized 和 ReentrantLock 的区别

相同点:都是可重复锁。

“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。

不同点:

1、底层实现上来说

synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法;

ReentrantLock 是从jdk1.5以来提供的API层面的锁(需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)

synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁(轻量级锁)、重量级锁。

ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。

2、因为ReentrantLock是用户自己调用的,所以控制权交给用户多一些

Synchronized ReentrantLock
等待可中断 不可 可以 - 调用interrupt()
锁是否手动释放 不需要 需要 - lock() 和 unlock()
是否公平锁 不公平 默认不公平,可以修改
锁是否可绑定条件Condition 不能 可以,是其一大特色

补充:

公平锁:先等待的线程先获得锁

绑定条件Condition:ReentrantLock通过绑定Condition,结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程

锁升级:

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。(在资源无竞争情况下消除了同步语句,连CAS操作都不做了,大白话就是对锁置个变量,如果发现为true,代表资源无竞争,则无需再走各种加锁/解锁流程。)

轻量级锁:当多个线程开始竞争同一把锁,但是在不同时段获取,JVM采用轻量级锁来避免线程的阻塞与唤醒。

通过适应性自旋(不断尝试获取锁,一般用循环实现,避免线程切换)来获得锁,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。尝试到一定次数时,就升级为重量级锁;

重量级锁:重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对比较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。

技术分享图片

Synchronized底层实现原理

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。

volatile关键字

好文

摘要:针对并发编程,我们常遇到三个问题

原子性、可见性、有序性

而votilate 只能保证 可见和部分有序

volatile关键字的两层含义:

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序

int x,y
volatile flag

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会将语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

可见性:-- JVM中的工作内存 和 主内存的 关系

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中重新读取新值(而非使用工作内存中的值)

但 votilate 不能保证对其修饰的变量的操作是原子性

public volatile int inc = 0;
public void increase() {
    inc++;
}
//当多个线程,对inc进行自增操作时,要注意【自增】操作本身不是原子性操作,它分为三步,1、去内存取值  2、+1  3、写回内存

此时只能使用 synchronized 或者 atomicInteger

public  int inc = 0;
public synchronized void increase() {
    inc++;
}

//或者使用原子操作类
public  AtomicInteger inc = new AtomicInteger();
public  void increase() {
    inc.getAndIncrement();
}

线程通信

1、管道

一是半双工的通信,数据只能单向流动;二是只能在具有亲缘关系的进程间使用

2、消息队列

由消息组成的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

3、共享内存

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。它往往与 信号量 配合使用,来实现进程间的同步和通信。

4、信号量

共享内存最大的问题是什么?线程安全问题。

如何解决这个问题?这个时候我们的信号量就上场了,信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。但其不能传递复杂消息,只能用来同步

5、Socket

悲观锁 和 乐观锁

Java中 synchronizedReentrantLock等独占锁就是悲观锁思想的实现,

在Java中 java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁适用于写比较少的情况下(多读场景),悲观锁适合多写的场景

  • CAS算法

乐观锁是一种思想,它其实并不是一种真正的『锁』

compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)

当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。

存在的问题:1、ABA 问题 2、循环时间长开销大 3、只能保证一个共享变量的原子操作

  • 为什么要使用池化技术?

1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

3、提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

  • 线程池参数

corePoolSize - 核心线程数大小 maximumPoolSize - 最大线程数

keepAliveTime - 如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。

unit -- keepAliveTime的时间单位

workQueue - 存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。

threadFactory - 用来创建线程工厂。比如这里面可以自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的。

handler - 拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。

Java多线程

原文:https://www.cnblogs.com/spongie/p/14675357.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!