一篇文章搞明白Java多线程

文章目录

一,线程概述

1,线程相关概念

(1)进程

  • 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动
  • 是操作系统进行资源分配与调度的基本单位.
  • 可以把进程简单的理解为正在操作系统中运行的一个程序.

(2)线程

  • 线程(thread)是进程的一个执行单元.
  • 一个线程就是进程中一个单一顺序的控制流,是进程的一个执行分支
  • 进程是线程的容器,一个进程至少有一个线程,一个进程中也可以有多个线程.
  • 在操作系统中是以进程为单位分配资源,如虚拟存储空间,文件描述符等
  • 每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储

(3)主线程与子线程

  • JVM 启动时会创建一个主线程,该主线程负责执行 main 方法 ,主线程就是运行main 方法的线程
  • Java 中的线程不是孤立的,线程之间存在一些联系. 如果在 A 线程中创建了 B 线程,称 B 线程为 A 线程的子线程,相应的 A 线程就是 B 线程的父线程

(4)串行,并行与并发

  • 串行:处理器(CPU)依次执行各个任务,只有当一个任务执行完毕才去执行下一个任务
  • 并行:多个处理器同时执行多个任务
  • 并发:对于一个处理器,在一个时间段内可以"同时"执行多个任务;微观上,在一个时刻只能执行一个任务

2,线程的创建与启动

线程的创建:在Java中一个线程对应着一个Thread类的实例,创建线程也就是创建一个Thread类的实例。
线程的启动:通过实例调用start()方法。
创建线程的具体的方式有:

1,继承Theard类并重写run()方法
例如:

public class Test1 {
    public static void main(String[] args) {
        new MyThread().start();
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}

2,实现Runnable接口并实现run()方法
例如:

public class Test1 {
    public static void main(String[] args) {
        new Thread(new MyThread()).start();
    }
}
class MyThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}

3,实现Callable接口并实现call()方法

具体内容后面介绍

4,通过线程池帮助我们创建线程

具体内容后面介绍

3,线程的常用方法

 1. public static native Thread currentThread();  //Thread.currentThread()方法可以获得当前线程
                                                  //Java 中的任何一段代码都是执行在某个线程当中的. 执行当前代码的线程就是当前线程.
          
 2. public static native void yield();            //释放当前线程的CPU持有权

 3. public static native void sleep(long millis) throws InterruptedException; //使当前线程休眠指定的时间

 4. public void interrupt(); //中断线程
                            //注意调用 interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程
                            
 5. public final native boolean isAlive();  //判断当前线程是否存活

 6. public final void setPriority(int newPriority);  //设置线程的优先级
                                                    //The minimum priority that a thread can have.
												    public static final int MIN_PRIORITY = 1;
												    //The default priority that is assigned to a thread.
												    public static final int NORM_PRIORITY = 5;
												    //The maximum priority that a thread can have.
												    public static final int MAX_PRIORITY = 10;
												    
 7. public final synchronized void setName(String name); //设置线程的名字

 8. public final String getName();  //获取线程的名字

 9. public final void join() throws InterruptedException; //在线程a中调用线程b的join(),此时线程a就进入阻塞状态,
                                                        // 直到线程b完全执行完以后,线程a才结束阻塞状态。
                                                        
 10. public final void setDaemon(boolean on); //将当前线程设置为守护线程

 11. public long getId(); //获取线程ID

 12. public State getState(); //获取线程状态

4,线程的生命周期

在这里插入图片描述

5,多线程的优势与存在的缺点

(1)多线程的优势:

  1. 提高系统的吞吐率(Throughout). 多线程编程可以使一个进程有多个并发(concurrent,即同时进行的)的操作
  2. 提高响应性(Responsiveness).Web 服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间
  3. 充分利用多核(Multicore)处理器资源. 通过多线程可以充分的利用 CPU 资源

(2)多线程编程存在的问题与风险:

  1. 线程安全(Thread safe)问题: 多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读取脏数据(过期的数据),,如丢失数据更新.
  2. 线程活性(thread liveness)问题: 由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非 RUNNABLE 状态,这就是线程活性问题,
    常见的活性故障有以下几种:
    (1) 死锁(Deadlock). 类似鹬蚌相争.
    (2) 锁死(Lockout), 类似于睡美人故事中王子挂了
    (3) 活锁(Livelock). 类似于小猫咬自己尾巴
    (4) 饥饿(Starvation).类似于健壮的雏鸟总是从母鸟嘴中抢到食物.
  3. 上下文切换(Context Switch): 线程的切换会消耗系统资源
  4. 可靠性: 可能会由一个线程导致 JVM 意外终止,其他的线程也无法执行.

二,线程安全问题

1,原子性

原子(Atomic)就是不可分割的意思
一个线程访问(读,写)某个共享变量的操作对于其他线程来说,要么已经执行完毕,要么尚未发生。

Java 有两种方式实现原子性: 一种是使用锁; 另一种利用处理器的CAS(Compare and Swap)指令.

锁具有排它性,保证共享变量在某一时刻只能被一个线程访问.

CAS 指令直接在硬件(处理器和内存)层次上实现,看作是硬件锁

2,可见性

在多线程环境中, 一个线程对某个共享变量进行更新之后 , 后续其他的线程可能无法立即读到这个更新的结果, 这就是线程安全问题的另外一种形式: 可见性(visibility).

如果一个线程对共享变量更新后, 后续访问该变量的其他线程可
以读到更新的结果, 称这个线程对共享变量的更新对其他线程可见,
否则称这个线程对共享变量的更新对其他线程不可见.

多线程程序因为可见性问题可能会导致其他线程读取到了旧数据
(脏数据).

3,有序性

有序性(Ordering)是指在某种情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器运行的其他线程看来是乱序的(Out of Order).

乱序是指内存访问操作的顺序看起来发生了变化

(1)重排序

编译器可能会改变两个操作的顺序,处理器也可能不会按照目标代码的顺序执行,这种现象称为重排序。

重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能.但是,可能对多线程程序的正确性产生影响,即可能导致线程安全问题。重排序与可见性问题类似,不是必然出现的.

可以把重排序分为指令重排序与存储子系统重排序两种.
指令重排序主要是由 JIT 编译器,处理器引起的, 指程序顺序与执行顺序不一样.
存储子系统重排序是由高速缓存,写缓冲器引起的, 感知顺序与执行顺序 不一致

(2)保证内存访问的有序性

可以使用 volatile 关键字, synchronized 关键字实现有序性

4,Java多线程内存模型

在这里插入图片描述
在这里插入图片描述

三,线程同步

1,线程同步机制简介

线程同步机制是一套用于协调线程之间的数据访问的机制,该机制可以保障线程安全.
Java 平台提供的线程同步机制包括: 锁,volatile 关键字,final 关键字,static关键字,以及相关的 API,如 Object.wait()/Object.notify()等

2,锁概述

线程安全问题的产生前提是多个线程并发访问共享数据.

将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问.锁就是复用这种思路来保障线程安全的

锁(Lock)可以理解为对共享数据进行保护的一个许可证. 对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须先持有该许可证. 一个线程只有在持有许可证的情况下才能对这些共享数据进行访问; 并且一个许可证一次只能被一个线程持有; 线程在结束对共享数据的访问后必须释放其持有的许可证

获得锁之后和释放锁之前这段时间所执行的代码称为临界区(Critical Section).
锁具有排他性(Exclusive), 即一个锁一次只能被一个线程持有.这种锁称为排它锁或互斥锁(Mutex).

JVM把锁分为内部锁和显示锁两种. 内部锁通过synchronized关键字实现; 显示锁通过 java.concurrent.locks.Lock 接口的实现类实现的

(1)锁的作用

锁可以实现对共享数据的安全访问. 保障线程的原子性,可见性与有序性.

锁是通过互斥保障原子性. 一个锁只能被一个线程持有, 这就保证临界区的代码一次只能被一个线程执行.使得临界区代码所执行的操作自然而然的具有不可分割的特性,即具备了原子性.

可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新缓存这两个动作实现的. 在 java 平台中,锁的获得隐含着刷新缓存的动作, 锁的释放隐含着冲刷处理器缓存的动作

锁能够保障有序性.写线程在临界区所执行的在读线程所执行的临界区看来像是完全按照源码顺序执行的

注意:使用锁保障线程的安全性,必须满足以下条件:
这些线程在访问共享数据时必须使用同一个锁
即使是读取共享数据的线程也需要使用同步锁

(2)锁相关概念

(1)可重入性

如果一个线程持有一个锁的时候还能够继续成功申请该锁,称该锁是可重入的, 否则就称该锁为不可重入的

(2)锁的争用与调度

Java 平台中内部锁属于非公平锁, 显示 Lock 锁既支持公平锁又支持非公平锁
对于等待锁的线程,如果先来先获得锁,则是公平锁;如果随机调度,则是非公平锁

(3)锁的粒度

一个锁可以保护的共享数据的数量大小称为锁的粒度.
锁保护共享数据量大,称该锁的粒度粗, 否则就称该锁的粒度细.
锁的粒度过粗会导致线程在申请锁时会进行不必要的等待.锁的粒度过细会增加锁调度的开销.

3,内部锁:synchronized关键字

Java 中的每个对象都有一个与之关联的内部锁(Intrinsic lock). 这种锁也称为监视器(Monitor), 这种内部锁是一种排他锁,可以保障原子性,可见性与有序性.
内部锁是通过 synchronized 关键字实现的.synchronized 关键字可以修饰代码块和方法

(1)synchronized 同步代码块

任何一个对象都可以被当作锁

public void mm(){
	synchronized ( this ) { //经常使用this当前对象作为锁对象
		for (int i = 1; i <= 100; i++) {
			System.out.println(Thread.currentThread().getName() + " --> " + i);
		}
	}
}

(2)synchronized同步方法

同步实例方法,默认this作为锁对象

public synchronized void mm22(){
	for (int i = 1; i <= 100; i++) {
		System.out.println(Thread.currentThread().getName() + " --> " + i);
		}
	}
}

同步静态方法,默认运行时类作为锁对象

public synchronized static void sm2(){
	for (int i = 1; i <= 100; i++) {
		System.out.println(Thread.currentThread().getName() + " --> " + i);
	}
}

(3)脏读

  • 出现读取属性值出现了一些意外, 读取的是中间值,而不是修改之后的值
  • 出现脏读的原因是对共享数据的修改与对共享数据的读取不同步
  • 解决方法:不仅对修改数据的代码块进行同步,还要对读取数据的代码块同步

(4)线程出现异常会自动释放锁

持有锁的线程出现异常而未处理的话,则该线程终止并释放锁

(5)死锁

  • 在多线程程序中,同步时可能需要使用多个锁,如果获得锁的顺序不一致,可能会导致死锁
  • 如何避免死锁:当需要获得多个锁时,所有线程获得锁的顺序保持一致即可

4,轻量级同步机制:volatile关键字

(1)volatile 的作用

保证数据的可见性。当读取使用volatile修饰的变量时,会刷新缓存;当将volatile修饰的遍历写出时,会冲刷写缓存

(2)volatile与synchronized的比较

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好;
  • volatile只能修饰变量,而 synchronized 可以修饰方法,代码块. 随着 JDK 新版本的发布,synchronized 的执行效率也有较大的提升,在开发中使用 sychronized 的比率还是很大的.
  • 多线程访问 volatile 变量不会发生阻塞,而 synchronized 可能会阻塞
  • volatile 能保证数据的可见性,但是不能保证原子性; 而synchronized 可以保证原子性,也可以保证可见性
  • 关键字 volatile 解决的是变量在多个线程之间的可见性;synchronized 关键字解决多个线程之间访问公共资源的同步性.

(3)常用原子类进行自增自减操作

我们知道 i++操作不是原子操作, 除了使用 Synchronized 进行同步外,也可以使用 AtomicInteger/AtomicLong 原子类进行实现

5,CAS

(1)CAS简介

CAS(Compare And Swap)是由硬件实现的.

CAS 可以将 read- modify - write 这类的操作转换为原子操作.

CAS 原理::在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(操作起始时读取的值)一样就更新;不一样则撤销本次操作

(2)ABA问题

CAS 实现原子操作背后有一个假设: 共享变量的当前值与当前线程提供的期望值相同, 就认为这个变量没有被其他线程修改过.
实际上这种假设不一定总是成立.如有共享变量 count = 0
A 线程对 count 值修改为 10
B 线程对 count 值修改为 20
C 线程对 count 值修改为 0
当前线程看到 count 变量的值现在是 0,现在是否认为 count 变量的值没有被其他线程更新呢? 这种结果是否能够接受??
这就是 CAS 中的 ABA 问题,即共享变量经历了 A->B->A 的更新.
是否能够接收 ABA 问题跟实现的算法有关.
如果想要规避 ABA 问题,可以为共享变量引入一个修订号(时间戳/版本号), 每次修改共享变量时,相应的修订号就会增加 1. 通过修订号依然可以准确判断变量是否被其他线程修改过. AtomicStampedReference 类就是基于这种思想产生的.

6,原子变量类

原子变量类基于CAS实现的,当对共享变量进行read-modify-write更新操作时,通过原子变量类可以保障操作的原子性与可见性.对变量的 read-modify-write 更新操作是指当前操作不是一个简单的赋值,而是变量的新值依赖变量的旧值,如自增操作i++。由于volatile只能保证可见性,无法保障原子性,原子变量类内部就是借助一个Volatile变量,并且保障了该变量的 read-modify-write 操作的原子性, 有时把原子变量类看作增强的 volatile 变量.

分组原子变量类
基础数据类型AtomicInteger, AtomicLong, AtomicBoolean
数组型AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
字段更新器AtomicIntegerFieldUpdater, AtomicLongFieldUpdater,AtomicReferenceFieldUpdater
引用型AtomicReference, AtomicStampedReference,AtomicMarkableReference

具体的操作可查看API

四,线程间的通信

1,等待/通知机制

(1)什么是等待通知机制

在多线程编程中,可能 A 线程的条件没有满足只是暂时的, 稍后其他的线程B可能会更新条件使得A线程的条件得到满足. 可以将 A线程暂停,直到它的条件得到满足后再将 A 线程唤醒.

(2)等待/通知机制的实现

  • Object 类中的 wait()方法可以使执行当前代码的线程等待,暂停执行,直到接到通知或被中断为止;该方法必须在同步方法或代码块中由锁对象调用并且调用 wait()方法,当前线程会释放锁
  • Object 类的 notify()可以唤醒线程,该方法也必须在同步方法或代码块中由锁对象调用 . 没有使用锁对象调用 wait()/notify() 会抛出IlegalMonitorStateExeption 异常. 如果有多个等待的线程,notify()方法只能唤醒其中的一个. 在同步代码块中调用 notify()方法后,并不会立即释放锁对象,需要等当前同步代码块执行完后才会释放锁对象,一般将 notify()方法放在同步代码块的最后.

(3)interrupt()方法会中断 wait()

当线程处于 wait()等待状态时, 调用线程对象的 interrupt()方法会中断线程的等待状态, 会产生 InterruptedException 异常

(4)notify()与 notifyAll()

notify()一次只能唤醒一个线程,如果有多个等待的线程,只能随机唤醒其中的某一个; 想要唤醒所有等待线程,需要调用 notifyAll().
注意:只能唤醒使用同一个锁对象wait的线程

(5)wait等待条件发生了变化

在使用 wait/nofity 模式时,注意 wait 条件发生了变化,也可能会造成逻辑的混乱
例如:有一条添加数据的线程,两条取数据的线程;可能出现添加一次取两次的情况,因为wait破环了原子性。如果wait是放在if代码块中,那么从wait出来时需要重新判断一下条件是否成立,这时可以将if改成while。

2,通过管道实现线程间通信

在java.io包中的PipeStream管道流用于在线程之间传送数据。一个线程发送数据到输出管道,一个线程从输入管道中读取数据。相关的类包括:PipedInputStream和PipedOutputStream,PipedReader和PipedWriter.
示例:

public class Demo2 {
    public static void main(String[] args) {
        Task task = new Task();
        PipedOutputStream os = new PipedOutputStream();
        PipedInputStream is = new PipedInputStream();
        try {
            is.connect(os);//输出管道流和输入管道流建立连接
        } catch (IOException e) {
            e.printStackTrace();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                try(os) {
                    task.send(os);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try(is) {
                    task.get(is);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
class Task{
    public void send(PipedOutputStream os) throws IOException {
        for (int i = 0; i < 20; i++) {
            String s = ""+i;
            os.write(s.getBytes());
        }
    }
    public void get(PipedInputStream is) throws IOException {
        byte[] bytes = new byte[10];
        int len;
        while ((len=is.read(bytes)) != -1){
            System.out.println(new String(bytes,0,len));
        }
    }
}

3,ThreadLocal

ThreadLocal用于线程之间的数据隔离,意为线程的局部变量
每个线程内部维护了一个Map(一个Entry数组),ThreadLocal的实例和其set的数据作为一对键值对存放当该线程的Map中,其中ThreadLocal实例为key,set的数据为value;

如何实现线程之间数据隔离?
答:ThreadLocal的实例和其set的数据在不同线程中会存放到不同的Map中,相互独立。

TheadLocal实例只能设置一个值,为什么用数组来存放?
答:在同一个线程中可能有多个ThreadLocal实例来存值,故需要一个数组

五,Lock显示锁

Lock的机制和synchronized类似,其提供了一些方法,功能更强大

1,Lock接口

(1)Lock的基本使用

调用lock()方法加锁,调用unlock()方法释放锁;通常使用try-finally结构,将unlock()放在finally中。
注意:多线程要想保持同步必须使用同一个Lock对象

(2)lockInterruptibly()方法

lockInterruptibly() 方法的作用:如果当前线程未被中断则获得锁,如果当前线程被中断则出现异常.

(3)tryLock()方法

tryLock(long time, TimeUnit unit) 的作用在给定等待时长内锁没有被另外的线程持有,并且当前线程也没有被中断,则获得该锁;通过该方法可以实现锁对象的限时等待.
tryLock()仅在调用时锁定未被其他线程持有的锁,如果调用方法时,锁对象被其他线程持有,则放弃. 调用方法尝试获得锁,如果该锁没有被其他线程占用则返回 true 表示锁定成功; 如果锁被其他线程占用则返回 false,不等待.

(4)newCondition()方法

关键字 synchronized 与 wait()/notify() 这两个方法一起使用可以实现等待/通知模式.。Lock 锁的 newContition() 方法返回 Condition 对象,Condition 类也可以实现等待/通知模式.

与wait()/notify()类似,Condition的await()用于等待并释放锁,signal()用于唤醒一个等待的线程,signalAll()用于唤醒所有等待的线程
注意:await()和signal()的使用都需要线程持有锁,由Condition对象调用,同一个Condition对象只能唤醒用同一个Condition对象等待的线程,并不能唤醒整个锁下所有等待的线程。

使用Condition的优势?
同一个Lock锁可以绑定多个Condition对象,这样我们可以通过某个Condition对象唤醒使用该Condition对象等待的线程,实现唤醒指定线程的功能。

例如:在多生产者/多消费者模式中,生产者线程使用同一个conditionA对象进行等待,消费者线程使用同一个conditionB对象进行等待;那么在生产者线程中,我们可以使用conditionB对象唤醒消费者线程,在消费者线程中,我们可以使用conditionA对象唤醒生产者线程,这样就避免了生产者唤醒生产者 / 消费者唤醒消费者的情况了

2,ReentrantLock 可重入锁

Lock接口的实现类

几个常用的方法:

  • int getHoldCount() 返回当前线程调用 lock()方法的次数
  • int getQueueLength() 返回正等待获得锁的线程预估数
  • int getWaitQueueLength(Condition condition) 返回与 Condition 条件相关的等待的线程预估数
  • boolean hasQueuedThread(Thread thread) 查询参数指定的线程是否在等待获得锁
  • boolean hasQueuedThreads() 查询是否还有线程在等待获得该锁
  • boolean hasWaiters(Condition condition) 查询是否有线程正在等待指定的 Condition 条件
  • boolean isFair() 判断是否为公平锁
  • boolean isHeldByCurrentThread() 判断当前线程是否持有该锁
  • boolean isLocked() 查询当前锁是否被线程持有

3,ReentrantReadWriteLock 读写锁

ReadWriteLock接口的实现类

synchronized 内部锁与 ReentrantLock 锁都是独占锁(排它锁), 同一时间只允许一个线程执行同步代码块,可以保证线程的安全性,但是执行效率低.
ReentrantReadWriteLock 读写锁是一种改进的排他锁,也可以称作共享/排他锁. 允许多个线程同时读取共享数据,但是一次只允许一个线程对共享数据进行更新.
总结:写写互斥、读写互斥、读读共享

使用方法:

        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();//获取读锁
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();//获取写锁

其他的使用方法与Lock类似。

六,线程的管理

1,线程组

类似于在计算机中使用文件夹管理文件,也可以使用线程组来管理线程. 在线程组中定义一组相似(相关)的线程,在线程组中也可以定义子线程组。

Thread 类有几个构造方法允许在创建线程时指定线程组,如果在创建线程时没有指定线程组则该线程就属于父线程所在的线程组.

JVM在创建main线程时会为它指定一个线程组,因此每个Java线程都有一个线程组与之关联, 可以调用线程的 getThreadGroup()方法返回线程组.

线程组开始是出于安全的考虑设计用来 区分不同的 Applet,然而ThreadGroup 并未实现这一目标,在新开发的系统中,已经不常用线程组, 现在一般会将一组相关的线程存入一个数组或一个集合中,如果仅仅是用来区分线程时,可以使用线程名称来区分, 多数情况下,可以忽略线程组.

2,捕获线程的执行异常

后续补充~

3,注入Hook钩子线程

后续补充~

4,线程池

(1)什么是线程池

在真实的生产环境中,可能需要很多线程来支撑整个应用,当线程数量非常多时 ,反而会耗尽 CPU 资源. 如果不对线程进行控制与管理,反而会影响程序的性能. 线程开销主要包括: 创建与启动线程的开销;线程销毁开销; 线程调度的开销; 线程数量受限 CPU 处理器数量.

线程池就是有效使用线程的一种常用方式. 线程池内部可以预先创建一定数量的工作线程,客户端代码直接将任务作为一个对象提交给线程池, 线程池将这些任务缓存在工作队列中, 线程池中的工作线程不断地从队列中取出任务并执行.线程池中的线程可以重复利用
在这里插入图片描述

(2)JDK对线程池的支持

JDK 提供了一套 Executor 框架,可以帮助开发人员有效的使用线程池

package test3;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo4 {
    public static void main(String[] args) {
        ExecutorService service1 = Executors.newSingleThreadExecutor();//创建单一线程池
        ExecutorService service2 = Executors.newFixedThreadPool(5);//创建定长线程池
        ExecutorService service3 = Executors.newCachedThreadPool();//创建缓存线程池,可动态扩容
        //通过execute提交一个Runnable任务
        service2.execute(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    if(i%2==0)
                        System.out.println(Thread.currentThread().getName()+" "+i);
                }
            }
        });
        //通过submit提交一个Callable任务
        service2.submit(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                for (int i = 1; i <= 10; i++) {
                    if(i%2!=0)
                        System.out.println(Thread.currentThread().getName()+" "+i);
                }
                return null;
            }
        });
        service2.shutdown();//关闭线程池
    }
}

(3)线程池的拓展

后续补充~~~~

1,核心线程池的底层实现
2,拒绝策略
3,ThrreadFactory
4,监控线程池
5,拓展线程池
6,优化线程池的大小
7,线程池死锁
8,线程池中的异常处理
9,ForkJoinPool线程池

七,保障线程安全的设计技术

从面向对象设计的角度出发介绍几种保障线程安全的设计技术,这些技术可以使得我们在不必借助锁的情况下保障线程安全,避免锁可能导致的问题及开销.

1,Java 运行时存储空间

Java运行时(Java runtime)空间可以分为栈区,堆区与方法区(非堆空间)。

栈空间(Stack Space)为线程的执行准备一段固定大小的存储空间,每个线程都有独立的线程栈空间,创建线程时就为线程分配栈空间。在线程栈中每调用一个方法就给方法分配一个栈帧,栈帧用于存储方法的局部变量,返回值等私有数据,即局部变量存储在栈空间中,基本类型变量也是存储在栈空间中,引用类型变量值也是存储在栈空间中,引用 的对象存储在堆中。由于线程栈是相互独立的,一个线程不能访问另外一个线程的栈空间,因此线程对局部变量以及只能通过当前线程的局部变量才能访问的对象进行的操作具有固定的线程安全性。

堆空间(Heap Space)用于存储对象,是在 JVM 启动时分配的一段可以动态扩容的内存空间.。创建对象时,在堆空间中给对象分配存储空间,实例变量就是存储在堆空间中的,堆空间是多个线程之间可以共享的空间,因此实例变量可以被多个线程共享。多个线程同时操作实例变量可能存在线程安全问题。

非堆空间(Non-Heap Space)用于存储常量,类的元数据等,非堆空间也是在 JVM 启动时分配的一段可以动态扩容的存储空间。类的元数据包括静态变量,类有哪些方法及这些方法的元数据(方法名,参数,返回值等)。非堆空间也是多个 线程可以共享的,因此访问非堆空间中的静态变量也可能存在线程安全问题。

堆空间和非堆空间是线程可以共享的空间,即实例变量与静态变量是线程可以共享的,可能存在线程安全问题。栈空间是线程私有的存储空间,局部变量存储在栈空间中,局部变量具有固定的线程安全性

2,无状态对象

对象就是数据及对数据操作的封装,对象所包含的数据称为对象的状态(State),实例变量与静态变量称为状态变量。如果一个类的同一个实例被多个线程共享并不会使这些线程存储共享的状态,那么该类的实例就称为无状态对象(Stateless Object).。反之如果一个类的实例被多个线程共享会使这些线程存在共享状态,那么该类的实例称为有状态对象。

实际上无状态对象就是不包含任何实例变量也不包含任何静态变量的对象。线程安全问题的前提是多个线程存在共享的数据,实现线程安全的一种办法就是避免在多个线程之间共享数据,使用无状态对象就是这种方法

3,不可变对象

不可变对象是指一经创建它的状态就保持不变的对象,不可变对象具有固有的线程安全性.。当不可变对象现实实体的状态发生变化时,系统会创建一个新的不可变对象,就如 String 字符串对象.。

一个不可变对象需要满足以下条件:

  • 类本身使用 final 修饰,防止通过创建子类来改变它的定义
  • 所有的字段都是 final 修饰的,final 字段在创建对象时必须显示初始化,不能被修改
  • 如果字段引用了其他状态可变的对象(集合,数组),则这些字段必须是 private 私有的

不可变对象主要的应用场景:

  • 被建模对象的状态变化不频繁
  • 同时对一组相关数据进行写操作,可以应用不可变对象,既可以保障原子性也可以避免锁的使用
  • 使用不可变对象作为安全可靠的Map键,HashMap键值对的存储位置与键的hashCode()有关,如果键的内部状态发生了变化会导致键的哈希码不同,可能会影响键值对的存储位置。如果 HashMap 的键是一个不可变对象,则 hashCode()方法的返回值恒定,存储位置是固定的.

4,线程特有对象

我们可以选择不共享非线程安全的对象,对于非线程安全的对象,每个线程都创建一个该对象的实例,各个线程线程访问各自创建的实例,一个线程不能访问另外一个线程创建的实例。这种各个线程创建各自的实例,一个实例只能被一个线程访问的对象就称为线程特有对象。 线程特有对象既保障了对非线程安全对象的访问的线程安全,又避免了锁的开销。线程特有对象也具有固有的线程安全性。

ThreadLocal类相当于线程访问其特有对象的代理,即各个线程通过 ThreadLocal 对象可以创建并访问各自的线程特有对象,泛型 T 指定了线程特有对象的类型。 一个线程可以使用不同的 ThreadLocal 实例来创建并访问不同的线程特有对象
在这里插入图片描述
ThreadLocal 实例为每个访问它的线程都关联了一个该线程特有的对象。

5,装饰器模式

装饰器模式可以用来实现线程安全,基本思想是为非线程安全的对象创建一个相应的线程安全的外包装对象,客户端代码不直接访问非线程安全的对象而是访问它的外包装对象.。外包装对象与非线程安全的对象具有相同的接口,即外包装对象的使用方式与非线程安全
对象的使用方式相同,而外包装对象内部通常会借助锁,以线程安全的方式调用相应的非线程安全对象的方法。

在 java.util.Collections 工具类中提供了一组 synchronizedXXX(xxx)方法,可以把不是线程安全的 xxx 集合转换为线程安全的集合,它就是采用了这种装饰器模式.。这个方法返回值就是指定集合的外包装对象。这类集合又称为同步集合.

使用装饰器模式的一个好处就是实现关注点分离,在这种设计中,实现同一组功能的对象的两个版本:非线程安全的对象与线程安全的对象.;对于非线程安全的在设计时只关注要实现的功能,对于线程安全的版本只关注线程安全性

八,锁的优化及注意事项

1,有助于提高锁性能的几点建议

(1)减少锁持有时间

对于使用锁进行并发控制的应用程序来说,如果单个线程特有锁的时间过长,会导致锁的竞争更加激烈,会影响系统的性能。在程序中需要尽可能减少线程对锁的持有时间,如下面代码:

public synchronized void syncMethod(){
	othercode1();
	mutexMethod();
	othercode();
}

在syncMethod同步方法中,假设只有mutexMethod()方法是需要同步的, othercode1()方法与 othercode2()方法不需要进行同步.。如果othercode1 与 othercode2 这两个方法需要花费较长的 CPU 时间,在并发量较大的情况下,这种同步方案会导致等待线程的大量增加。一个较好的优化方案是,只在必要时进行同步,可以减少锁的持有时间,提高系统的吞吐量,如把上面的代码改为:

public void syncMethod(){
	othercode1();
	synchronized (this) {
		mutexMethod();
	}
	othercode();
}

只对 mutexMethod()方法进行同步,这种减少锁持有时间有助于降低锁冲突的可能性,提升系统的并发能力.

(2)减小锁的粒度

一个锁保护的共享数据的数量大小称为锁的粒度。如果一个锁保护的共享数据的数量大就称该锁的粒度粗,否则称该锁的粒度细。锁的粒度过粗会导致线程在申请锁时需要进行不必要的等待。减少锁粒度是一种削弱多线程锁竞争的一种手段,可以提高系统的并发性在JDK7前,java.util.concurrent.ConcurrentHashMap 类采用分段锁协议,可以提高程序的并发性;即对每个链表加锁,而不是对整个HashMap加锁

(3)使用读写分离锁代替独占锁

使用ReadWriteLock读写分离锁可以提高系统性能,使用读写分离锁也是减小锁粒度的一种特殊情况。第二条建议是能分割数据结构实现减小锁的粒度,那么读写锁是对系统功能点的分割。
在多数情况下都允许多个线程同时读,在写的使用采用独占锁,在读多写少的情况下,使用读写锁可以大大提高系统的并发能力.

(4)锁分离

将读写锁的思想进一步延伸就是锁分离。读写锁是根据读写操作功能上的不同进行了锁分离。根据应用程序功能的特点,也可以对独占锁进行分离,
如 java.util.concurrent.LinkedBlockingQueue 类中 take()与put()方法分别从队头取数据,把数据添加到队尾。虽然这两个方法都是对队列进行修改操作,由于操作的主体是链表,take()操作的是链表的头部,put()操作的是链表的尾部,两者并不冲突。如果采用独占锁的话,这两个操作不能同时并发。在该类中就采用锁分离,take()取数据时有取锁,,put()添加数据时有自己的添加锁,这样 take()与 put()相互独立实现了并发.

注:(2)、(3)、(4)这三种方式思想类似

(5)粗锁化

为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短。但是凡事都有一个度,如果对同一个锁不断的进行请求,同步和释放,也会消耗系统资源。如:

public void method1(){
	synchronized( lock ){
		同步代码块 1
	}
	synchronized( lock ){
		同步代码块 2
	}
}

JVM在遇到一连串不断对同一个锁进行请求和释放操作时,会把所有的锁整合成对锁的一次请求,从而减少对锁的请求次数,这个操作叫锁的粗化,如上一段代码会整合为:

public void method1(){
	synchronized( lock ){
		同步代码块 1
		同步代码块 2
	}
}

在开发过程中,也应该有意识的在合理的场合进行锁的粗化,尤其在循环体内请求锁时,如:

for(int i = 0 ; i< 100; i++){
	synchronized(lock){}
}

这种情况下,意味着每次循环都需要申请锁和释放锁,所以一种更合理的做法就是在循环外请求一次锁,如:

synchronized( lock ){
	for(int i = 0 ; i< 100; i++){}
}

2,JVM对锁的优化

(1)锁偏向

锁偏向是一种针对加锁操作的优化,如果一个线程获得了锁,那么锁就进入偏向模式, 当这个线程再次请求锁时,无须再做任何同步操作,这样可以节省有关锁申请的时间,提高了程序的性能。

锁偏向在没有锁竞争的场合可以有较好的优化效果,对于锁竞争比较激烈的场景,效果不佳,锁竞争激烈的情况下可能是每次都是不同的线程来请求锁,这时偏向模式失效.

(2)轻量级锁

如果锁偏向失败,JVM 不会立即挂起线程,还会使用一种称为轻量级锁的优化手段。会将对象的头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,就进入临界区。如果获得轻量级锁失败,表示其他线程抢到了锁,那么当前线程的锁的请求就膨胀为重量级锁。当前线程就转到阻塞队列中变为阻塞状态。

偏向锁,轻量级锁都是乐观锁,重量级锁是悲观锁

一个对象刚开始实例化时,没有任何线程访问它,它是可偏向的,即它认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程。偏向第一个线程,这个线程在修改对象头成为偏向锁时使用 CAS 操作,将对象头中 ThreadId 改成自己的 ID,之后再访问这个对象时,只需要对比 ID 即可。一旦有第二个线程访问该对象,因
为偏向锁不会主动释放,所以第二个线程可以查看对象的偏向状态,当二个线程访问对象时,表示在这个对象上已经存在竞争了,检查原来持有对象锁的线程是否存活,如果挂了则将对象变为无锁状态,然后重新偏向新的线程; 如果原来的线程依然存活,则马上执行原来线程的操作栈,检查该对象的使用情况,如果仍然需要偏向锁,则偏向锁升级为轻量级锁.

轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对同一个锁的操作会错开,或者稍微等待一下(自旋)另外一个线程就会释放锁。当自旋超过一定次数,或者一个线程持有锁,一个线程在自旋,又来第三个线程访问时,,轻量级锁会膨胀为重量级锁, 重量级锁除了持有锁的线程外,其他的线程都阻塞.

注:本文参考Java并发编程实战、动力节点教程、尚硅谷教程、开课吧教程

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我待Java如初恋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值