Java线程
一、基础
1、多线程概念
1.1 进程和线程
- 什么是进程?
一个执行中的程序
- 什么是线程?
- 区别
线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。在Java中,当我们启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。
1.2线程的组成部分
- 一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成
1.3 多线程的利弊:
利:
-
资源利用率更好
-
程序设计在某些情况下更简
-
程序响应更快
弊:
- 使程序设计更复杂
- 增加线程切换开销
- 资源开销增大
2、创建线程【重点】
2.1 创建线程的两种方式
- 继承Thread类并重写run的方法,
- 实现Runnable接口的run方法,
通过继承Thread类
public class ExtendsThread {
/**
* 基于extends继承Thread类,创建线程
* */
public static void main(String[] args){
//第一步:创建自定义线程类的一个对象。
MyThread myThread = new MyThread();
//第二步:调用该对象的start()方法启动线程
myThread.start();
//mt.run();//这属于方法调用,不是启动线程
//在主线程中运行的代码,除了main线程之外的线程称为子线程
for(int i=0;i<100;i++){
System.out.println("主线程man()method执行中!");
}
}
}
/**
* 自己要创建一个线程,就要自定义一个类,让该类继承java提供的Thread类,
* 并且重写Thread类的run方法
*/
class MyThread extends Thread{
//重写run()方法,当线性启动时会执行run方法
@Override
public void run(){
for(int i=10;i<=2000;i++){}
System.out.println("Override run method,线程Thread启动,run方法执行了");
}
}
实现Runnable接口
public class ImplementsRunnable {
public static void main(String[] args) {
//基于接口实现的线程怎么启动
//第一步,创建一个我们自定义类的对象,该类实现了Runnable接口
Mythread mythread = new Mythread();
//第二步:创建一个线程类,并把我们自定义的Runnable类作为该线程的构造参数传入
Thread thread = new Thread(mythread);
Thread.currentThread().setName("main Thread");
//第三步:调用线程的start()方法启动线程
thread.start();
//在主线程中运行的代码
for (int i=0;i<500;i++){
if(i>400&&i<420){
System.out.println(Thread.currentThread().getName()+"-"+i);
}
}
}
}
/**
* 通过实现接口的方式自定义线程。实现接口Runnable接口,
* 并且实现接口里抽象方法run
*/
class Mythread implements Runnable{
@Override
public void run(){
for(int i=0;i<500;i++){
if(i>490){
System.out.println(i+"-MyThread.name(id)-"+
Thread.currentThread().getName());
}
}
}
}
2.3两者的区别
- 使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递,
- 而如果使用Runnable方式,因为Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runable则没有这个限制。
2.4启动线程需要注意的问题
- 1.不要调用run方法
- 2、一个线程只能调用一次start方法**,如果调用多次会出异常java.lang.IllegalThreadStateException**
2.5 线程的状态(基本)
初始状态 ------ 就绪状态 ------- 运行状态 -------- 终止状态
2.6 线程常见的方法
- 1、设置线程名称 (setName、getName、Thread.currentThread获取当前线程对象)
- 2.设置线程的优先级(setPriority、getPriority),不一定生效
- 3、线程休眠 (Thread.sleep(毫秒数))
- 4、线程礼让(Thread.yield()),但不一定成功*
- 5、线程加入(join()) 如果线程出现join操作,那么调用的线程将会阻塞。等到被调用的线程执行结束。
Thread.sleep
package JAVA线程_Thread.Thread_method;
public class Method_sleep {
public static void main(String[] args) {
//线程1 sleep(1000);
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getId() + "输出数字:" + i);
}
}
});
//线程2 sleep(1000);
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 10; i <= 20; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getId() + "输出数字:" + i);
}
}
});
thread1.start();
thread2.start();
}
}
Thread.yield
public class Method_yield extends Thread{
/**
* 4、线程礼让(Thread.yield()),但不一定成功
* 礼让当次,不会一直礼让!!<即会失效>
* */
public static void main(String[] args) {
new Method_yield().start();
new Method_yield().start();
new Method_yield().start();
}
@Override
public void run(){
for (int i= 1;i<=10;i++){
if(i==8){
System.out.println(Thread.currentThread().getId()+"-"
+Thread.currentThread().getName()+"线程礼让!");
Thread.yield();
}else {
System.out.println(Thread.currentThread().getId()+"-"
+Thread.currentThread().getName()+"当前i值="+i);
}
}
}
}
join
public class Method_Join {
public static void main(String[] args) throws InterruptedException {
//创建线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 10; i <= 20; i++) {
System.out.println(Thread.currentThread().getName()
+ "-" + Thread.currentThread().getId()
+ "-输出:" + i);
}
}
});
thread.setName("<MyThread>");
thread.start();
/**
* 必须要先启动线程static() 后才能在其它线程执行join()
* */
//主线程执行路径
for(int i=10;i<=20;i++){
if(i==15) thread.join();
System.out.println(Thread.currentThread().getId()+"-"
+Thread.currentThread().getName()+"-输出结果:"+i);
}
/**
* 5、线程加入(join()) 如果线程出现join操作,那么调用的线程将会阻塞。
* 等到被调用的线程执行结束。
*/
}
可使用匿名内部类/Lambda表达式 来implements实现Runnable/Callable接口interfac
2.7 线程的状态
初始状态 ------ 就绪状态 ------- 运行状态 ----- 限期等待 *-----*无限期等待 -------- 终止状态
2.8 线程的分类
daemon线程(守护线程)比如垃圾回收线程,jvm不会等待此类线程结束自己才退出。
user线程(用户线程)只要有一个用户线程还没结束,正常情况下JVM就不会退出。
2.8.1 守护线程?
创建守护线程
守护线程与用户线程示例
public class User_Daemon_Thread {
public static void main(String[] args) {
//创建线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 10; i < 200; i++) {
System.out.println(Thread.currentThread().getName()+":输出-"+i);
}
}
});
//设置为Daemon_thread
thread.setDaemon(true);
thread.setName("DaemonThread");
thread.start();
/**
* daemon线程(守护线程)比如垃圾回收线程,
* jvm不会等待此类线程结束自己才退出。
*
* 设置为守护线程 setDaemon();
* */
//主线程main:
for(int i=1;i<=15;i++){
System.out.println(Thread.currentThread().getName()+"输出:"+i);
}
}
}
3、线程安全【重点】
3.1 线程安全问题
3.1.1 共享资源
所谓共享资源,就是同一份资源被多个线程所持有或者说多个线程访问。线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题,因此在访问共享资源时需要要保证同步,保证被操作资源(比如变量)的原子性。
3.2 线程同步
java中使用关键字synchronized来实现线程的同步
3.2.3 同步方式
- 方式一:同步代码块
- 方式二:同步方法
3.3 线程同步买票案例
3.3.1 线程不安全
-
循环中的代码并非原子操作,所以在线程执行的过程中,会有其他线程执行,会造成
ticket < 0无效
-
ticket 并非原子操作,其包含有三个步骤
public class NoSynchronized_demo {
public static void main(String[] args) {
TicketRunable ticketRunable1 = new TicketRunable();
TicketRunable ticketRunable2 = new TicketRunable();
TicketRunable ticketRunable3 = new TicketRunable();
ticketRunable1.start();
ticketRunable2.start();
ticketRunable3.start();
}
}
/** Ticket 票、入场卷 */
class TicketRunable extends Thread{
/**
* 多个线程同时读写一个共享资源并且没有任何同步措施时,
* 导致出现脏数据或者其他不可预见的结果的问题
* */
static int ticket = 10; //票总数
public void run(){ //买票流程
while(true){
if(ticket<=0){
System.out.println(Thread.currentThread().getId()+":票销售空!!!!");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getId()+"售出票号:"+ticket--);
}
}
}
3.3.2 同步代码块解决
public class Synchronized_CodeBlock {
public static void main(String[] args) {
new Thread(new TicketRunnable()).start();
new Thread(new TicketRunnable()).start();
new Thread(new TicketRunnable()).start();
}
}
/**
*同步代码块-解决线程共享资源的问题
* 互斥锁对象(可以是任意的java对象,但是要保证对象唯一)
*/
class TicketRunnable implements Runnable{
static int ticket = 10;
/** 创建锁对象 */
static final Object lock_obj = new Object();
@Override
public void run(){
while(true){
synchronized(lock_obj) {
/**
* 也需要放入 synchronized
* 应为可能会判断通过后,一直拿不到锁,知道执行完拿到锁,然后卖出负票!
* */
if (ticket < 1) {
System.out.println(Thread.currentThread().getId() + "-已售空!");
break;
}
try {
Thread.sleep(01000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getId() + "-售出:" + ticket--);
}
Thread.currentThread().yield(); /** 礼让无效 */
}
}
}
3.3.3 同步方法解决
public class Synchronized_Method {
public static void main(String[] args) {
new Thread(new TicketRunable3()).start();
new Thread(new TicketRunable3()).start();
new Thread(new TicketRunable3()).start();
}
}
class TicketRunable3 implements Runnable{
static int tickets = 15;
final Object lock_obj = new Object();
@Override
public void run(){
while(true) sale();
}
/**同步方法的锁对象是this,所以要保证当前对象的唯一 */
public synchronized void sale(){
if(tickets<1){
System.out.println(Thread.currentThread().getId()+"—售空!");
System.exit(-1);
}
System.out.println(Thread.currentThread().getId()+"售出:"+tickets--);
}
}
3.4 synchronized关键字
synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。另外,由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换
什么是用户态和内核态?(了解)
4、线程的生命周期[重点]
线程的5种状态:创建状态---->就绪状态---->运行状态---->阻塞\等待状态---->终止状态
二、进阶
1、线程死锁[理解]
1.1 什么是死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
1.2 死锁示例
public class Dead_Lock {
public static void main(String[] args) throws InterruptedException {
new Thread(new Girl()).start();
//Thread.sleep(3000);
new Thread(new Boy()).start();
}
}
class Lock{
static final Object left = new Object(); //锁对象1
static final Object right = new Object(); //锁对象2
}
class Boy implements Runnable{ //线程1
@Override
public void run() {
synchronized(Lock.left){
System.out.println("Boy拿到Left_Lock");
synchronized(Lock.right){
System.out.println("Boy拿到right_Lock");
}
}
}
}
class Girl implements Runnable{ //线程2
@Override
public void run() {
synchronized(Lock.right){
System.out.println("Girl拿到right_Lock");
synchronized(Lock.left){
System.out.println("Girl拿到Left_Lock");
}
}
}
}
1.3 如何避免死锁?
各线程之间访问资源保持顺序性, 如上的男孩女孩获取筷子的示例,我们只需要让男孩线程和女孩线程获取筷子的顺序保持一致,就可以避免死锁。
public class Dead_Lock_规避 {
/**
* 让线程获取锁对象的顺序保持一致,就可以避免死锁。
* */
public static void main(String[] args) {
//线程1
new Thread(()->{
synchronized(Lock.left){
System.out.println("Boy获取了左手!");
synchronized(Lock.right){
System.out.println("Boy获得了右手!");
}
}
}).start();
//线程1
new Thread(()->{
synchronized(Lock.left){
System.out.println("Girl获取了左手!");
synchronized(Lock.right){
System.out.println("Girl获得了右手!");
}
}
}).start();
}
}
2、 线程通信[理解]
线程通信:线程之间可以通过共享内存的方式通信。
若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。
-
wait() 调用该对象的某个线程会阻塞挂起
-
notify() 调用该对象的notify方法会唤醒在该对象上调用wait等待中的线程。具体是哪个线程,是随机的。
-
notifyAll()
Sleep和wait的区别:sleep会抱着锁睡,wait进入等待时会释放自己持有的锁。
public class ProducersAndConsumers {
static final Shop shop = new Shop();
public static void main(String[] args) {
//生产者线程
new Thread(()->{
for(int i=1;i<=9;i++){
try {
shop.putProducts(new Product("产品"+i));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//消费者线程
new Thread(()->{
for (int i=1;i<=9;i++){
try {
shop.getProduct();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
/** 商店类
*
* 生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,
* 生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费
* */
class Shop{
/**
* 显然生产者和消费者之间必须保持同步,
* 即不允许消费者到一个空的缓冲区中取产品,
* 也不允许生产者向一个满的缓冲区中放入产品。
* */
private List<Product> products = new LinkedList<>();//存放产品
//进货(生产)
public synchronized void putProducts(Product pro) throws InterruptedException {
if(products.size()>=3){ //表示仓库满了,需要等待出售后在进货
System.out.println("满仓!![等待....]");
this.wait(); /** 调用该方法的对象,所在的线程,进入睡眠 */
}
//表示没有商品需要生产
System.out.println("生产产品中!");
//1、模拟生产商品
products.add(pro);
//2、通过消费者进行消费
this.notify(); /** 调用该方法的对象,所在的线程,的其它线程苏醒 */
}
//卖货(消费)
public synchronized void getProduct() throws InterruptedException {
if(products.size()<=0){ //表示没有商品需要等待
System.out.println("空仓![等待生产....]");
this.wait();
}
//表示有商品需要消费
// 1、模拟消费商品
System.out.print("销售出一个商品:");
System.out.println(products.remove(products.size()-1));
//2、通知生产者生产
this.notify();
}
}
/** 产品类 */
class Product{
private String name;
public Product(String name) {
this.name = name;
}
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
'}';
}
}
3、线程池
3.1 线程池概念
- 如果有非常的多的任务需要多线程来完成,且每个线程执行时间不会太长,这样频繁的创建和销毁线程。
- 频繁创建和销毁线程会比较耗性能。有了线程池就不要创建更多的线程来完成任务,因为线程可以重用
- 线程池用维护者一个队列,队列中保存着处于等待(空闲)状态的线程。不用每次都创建新的线程。
线程是如何被创建出来的(了解)
3.2 线程池实现逻辑
假设我们线程池里只有5个线程可用,但这时候来了50个请求,我们该怎么处理?
3.3 线程池中常见的类
常用的线程池接口和类**(所在包java.util.concurrent)**。
Executor:线程池的顶级接口
ExecutorService:线程池接口,可通过submit(Runnable task)* 提交任务代码。
Executors工厂类:通过此类可以获得一个线程池。
方法名 | 描述 |
---|---|
newFixedThreadPool(int nThreads) | 获取固定数量的线程池。参数:指定线程池中线程的数量。 |
newCachedThreadPool() | 获得动态数量的线程池,如不够则创建新的。 |
import com.mchange.v2.async.ThreadPoolAsynchronousRunner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 第一步:自定义一个线程类,基于实现runnable接口的形式
*/
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("这是通过线程池启动的一个任务"+Thread.currentThread().getName());
}
}
public class NewFixedThreaPool_固定 {
/**
* 使用线程池技术来执行自定义的线程任务
*/
//第二步:定义一个测试方法
public static void main(String[] args) {
//第三步:使用线程池工厂获取一个线程池newFixedThreadPool(
ExecutorService executor = Executors.newFixedThreadPool(2);
//第四步:创建自定义线程的一个对象
MyThread myThread01 = new MyThread();
//第五步:把自定义线程对象的引用通过线程池的submit方法提交给线程池执行
executor.submit(myThread01);
//打印主线程名
System.out.println(Thread.currentThread().getName());
//第六步:关闭线程池
executor.shutdown();
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 第一步:自定义一个线程类,基于实现runnable接口的形式
*/
public class NewCachedThreadPool {
/**
* 使用无边界线程池执行线程任务
*/
public static void main(String[] args) {
//通过线程池工厂类或一个线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//往线程池里添加任务,任务通过匿名内部类的形式提供
executorService.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
});
executorService.submit(()->{ //采用Lambda表达式创建
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
});
//使用完线程池一定要记得关闭
executorService.shutdown();
}
}
3.4 Callable接口
- JDK1.5加入,与Runnable接口类似,实现之后代表一个线程任务。
- Callable具有泛型返回值、可以声明异常。
public interface Callable< V >{ public V call() throwsException; }
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Callable接口 {
/**
* • JDK1.5加入,与Runnable接口类似,实现之后代表一个线程任务。
* • Callable具有泛型返回值、可以声明异常。
* public interface Callable< V >{ public V call() throws Exception; }
* */
public static void main(String[] args) {
//1、创建线程池对象
ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService1 = Executors.newFixedThreadPool(3);
//2、通过线程池提交线程并执行任务
executorService.submit(new Callable<Object>(){
@Override
public Object call() throws Exception{
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
return null;
}
});
executorService.submit(() ->{
System.out.println(Thread.currentThread().getName());
return null;
});
//3、关闭线程池
executorService.shutdown();
}
}
Runnable接口和Callable接口的区别?
1、这两个接口都可以当做线程任务提交并执行
2、Callable接口执行完线程任务之后有返回值,而Runnable接口没有返回值
3、Callable接口中的call方法已经抛出了异常,而Runnable接口不能抛出编译异常
3.5 Future接口
- Future接口表示将要执行完任务的结果。
- get()以阻塞形式等待Future中的异步处理结果(call()的返回值)。
package JAVA线程_Thread;
import javax.print.DocFlavor;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* @className: Future_接口
* @Author: ich liebe Dich
* @Date: 2022/9/27 23:53
* @Version: 1.0
*/
public class Future_接口 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* Runnable接口和Callable接口的区别?
* 1、这两个接口都可以当做线程任务提交并执行
* 2、Callable接口执行完线程任务之后有返回值,而Runnable接口没有返回值
* 3、Callable接口中的call方法已经抛出了异常,而Runnable接口不能抛出编译异常
* Future接口:
* 用于接口Callable线程任务的返回值。
* get()方法当线程任务执行完成之后才能获取返回值,这个方法是一个阻塞式的方法
*/
//1、创建线程池对象
ExecutorService executorService = Executors.newCachedThreadPool();
//2、通过线程池提交线程并执行任务
Future<String> future = executorService.submit(() -> {
try {
Thread.sleep(3000); //睡眠加长运行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
String str = "3秒后,任务执行结束!";
return str;
});
//获取线程任务的返回值
System.out.println(future.get()); // get() 阻塞式的方法
//测试主线程被阻塞:
System.out.println(Thread.currentThread().getName()+":被阻塞到现在执行!");
//3、关闭线程池
executorService.shutdown();
}
}
案例:计算1-1000结果,使用四个线程分别计算?即:第一个线程计算1-250第二个 251~500 …
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Demo_Pool_Callable_Future {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* 案例:计算1-1000结果,使用四个线程分别计算?即:第一个线程计算1-250第二个 251~500 ...
* */
ExecutorService executorService = Executors.newCachedThreadPool();
Future<Integer> future01 = executorService.submit(() -> {
int total = 0;
for (int i = 1; i <= 250; i++) {
total += i;
}
return total;
});
Future<Integer> future02 = executorService.submit(() -> {
int total = 0;
for (int i = 256; i <= 500; i++) {
total += i;
}
return total;
});
Future<Integer> future03 = executorService.submit(() -> {
int total = 0;
for (int i = 501; i <= 1000; i++) {
total += i;
}
return total;
});
System.out.println("总和为:"+(future01.get()+future02.get()+future03.get()));
executorService.shutdown();
}
}
3.6 CountDownLatch
CountDownLatch 的构造函数接收一个 int 类型的参数作为计数器,如果你想等待 N个点完成,这里就传入 N。
当我们调用 CountDownLatch 的 countDown 方法时,N就会减 1,CountDownLatch 的 await 方法会阻塞当前线程,直到 N 变成零。
public class CountDownLatch_计数器 {
/**
* CountDownLatch 的 countDown 方法时,N 就会减 1,
* CountDownLatch 的 await 方法会阻塞当前线程,直到 N 变成零
*
* ---
* 并且该线程中须要先调用wait()在调用countdown()直到N--为0,该线程才能结束阻塞状态!
* */
static CountDownLatch countdownLatch = new CountDownLatch(2);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.submit(()->{ //线程一,countdownLatch.wait()
/* countdownLatch.countDown();
countdownLatch.countDown();*/
try {
countdownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("countdownLatch中的N清另,线程一执行结束!");
});
countdownLatch.countDown();
System.out.println(Thread.currentThread().getName()+":主线程执行了");
//线程二:在该线程中执行countdownLatch.countdown()方法
executorService.submit(()->{
countdownLatch.countDown();
System.out.println("线程二执行了。+ countdownLatch.countdown() ");
});
executorService.shutdown();
}
}
3.7 Exchanger
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange()方法,它会一直等待第二个线程也执行 exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。下面来看一下 Exchanger 的应用场景。
使用场景:Exchanger 也可以用于校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用 AB 岗两人进行录入,录入到 Excel 之后,系统需要加载这两个 Excel,并对两个Excel 数据进行校对,看看是否录入一致。
public class Exchanger_交换者接口 {
/**
* Exchanger 用于进行线程间的数据交换。两个线程通过 exchange() 方法交换数据。
* 如果第一个线程先执行 exchange()方法,它会一直等待第二个线程也执行 exchange 方法
*
* 1.必须要<-->泛型指定交换的数据类型
* 2.exchange()方法会抛出异常[InterruptedException]fan'x
* */
//线程间协作的工具类
static Exchanger<String> exchanger = new Exchanger<>();
public static void main(String[] args) {
//ExecutorService Fixed = Executors.newFixedThreadPool(2);
ExecutorService Cached = Executors.newCachedThreadPool();
//线程一
Cached.submit(()->{
try {
String A = "银行流水A";
System.out.println("线性一:流水数据A已传递出去!");
String B = exchanger.exchange(A);
System.out.println("线程一拿到线程一传递过来的数据:"+B);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//线程二
Cached.submit(()->{
try {
String B = "银行流水B";
System.out.println("线程二:流水数据B传递");
String A = exchanger.exchange(B);
System.out.println("线程二拿到线程一传递过来的数据:"+A);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
三、高级
1、Lock锁
1.1 Lock锁
-
ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入一个阻塞队列里面。
-
JDK1.5加入,与synchronized比较,显示定义,结构更灵活。
-
提供更多实用性方法,功能更强大、性能更优越。
ReentrantLock:
- Lock接口的实现类,与synchronized一样具有互斥锁功能。
随堂练习:用 ReentrantLock来实现一个简单的线程安全的 list
public class ReentrantLock_Lock锁 {
public static void main(String[] args) {
//1.创建线程池
//ExecutorService Fixed = Executors.newFixedThreadPool(3);
ExecutorService Cached = Executors.newCachedThreadPool();
//2.创建线性(任务)对象
MyThread_Ticket ticket = new MyThread_Ticket();
Cached.submit(ticket); //启动线程池3条
Cached.submit(ticket);
Cached.shutdown(); //关闭线程池
}
}
//创建线程任务类:
class MyThread_Ticket implements Runnable{
static int ticket = 100; //ticket总票数
/**
* ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,
* 其他获取该锁的线程会被阻塞而被放入一个阻塞队列里面。
*
* jdk1.5之后加入的--重入锁--Reentrant
* (Reentrant).lock()--上锁;
* (Reentrant).unlock()--释放锁;
* */
Lock reentratlock = new ReentrantLock(); //Lock锁
Object obj = new Object();
@Override
public void run() {
while (true){
try{
reentratlock.lock(); //上锁!
if(ticket<=0) break;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"卖出票->"+ticket--);
}finally {
reentratlock.unlock(); //释放锁!
}
}
}
}
案例:用 ReentrantLock来实现一个简单的线程安全的 list
public class Demo1_ReentrantLock_List {
/**
* 用 ReentrantLock来实现一个简单的线程安全的 list
* */
public static void main(String[] args) throws InterruptedException {
ExecutorService Cached = Executors.newCachedThreadPool();
ReentrantLock_List Mylist = new ReentrantLock_List();
//启动线程池
Cached.submit(()->{ //1-5
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0;i<5;i++){
System.out.println(Thread.currentThread().getName()+":add");
Mylist.list.add(i);
}
});
Cached.submit(()->{ //5-10
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i =5;i<10;i++) {
System.out.println(Thread.currentThread().getName()+":add");
Mylist.list.add(i);
}
});
Cached.submit(()->{ //1-20
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i =10;i<=20;i++){
System.out.println(Thread.currentThread().getName()+":add");
Mylist.list.add(i);
}
});
Thread.sleep(1500);
System.out.println(Mylist.list);
Cached.shutdown();
}
}
//创建线程安全的List : ReentrantLock_List
class ReentrantLock_List{
List<Integer> list = new ArrayList<>();
//创建锁
ReentrantLock reentrantLock = new ReentrantLock();
public void add(int val){
try{
reentrantLock.lock();
list.add(val);
}finally {
reentrantLock.unlock();
}
}
}
1.2 读写锁
ReentrantReadWriteLock:
- 一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。
- 支持多次分配读锁,使多个读操作可以并发执行。
互斥规则:
-
写-写:互斥,阻塞。
-
读-写:互斥,读阻塞写、写阻塞读。
-
读**-**读:不互斥、不阻塞。
-
在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率。
public class ReentratReadWriteLock_读写锁 {
static User user = new User(); //创建用户类实例
static int i =1;
public static void main(String[] args) {
//创建side=10的线程池
ExecutorService Fixed = Executors.newFixedThreadPool(10);
//系统当前时间 -> 开始时间
long start = System.currentTimeMillis();
//创建8个读操作线程,2个写操作线程
for(int i =1;i<=2;i++){
Fixed.submit(new WriteThread());
}
for(int i = 1;i<=8;i++){
Fixed.submit(new ReadThread());
}
Fixed.shutdown();
/**
* 判断线程池中的任务是否执行结束,如果结束了返回true--代码空转
* */
while(true){
if(Fixed.isTerminated()) break;
}
long end = System.currentTimeMillis();
System.out.println("共耗时:"+(end-start)+"毫秒");
}
}
//读操作线程任务
class ReadThread implements Runnable{
@Override
public void run() {
System.out.println(ReentratReadWriteLock_读写锁.user.getName());
}
}
//写操作线程任务
class WriteThread implements Runnable{
@Override
public void run() {
ReentratReadWriteLock_读写锁.user.setName("cxk"+(ReentratReadWriteLock_读写锁.i++));
}
}
/**
*1.一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁
*
* ReadWriteLock lock = new ReentrantReadWriteLock(); 创建锁对象
* Lock readlock = lock.readLock(); 创建读锁
* readlock.lock(); //上读锁
* readlock.unlock(); //释放读锁
*
* Lock writelock = lock.writeLock(); 创建写锁
* writelock.lock(); //上写锁
* writelock.unlock(); //释放写锁
* */
//用户类
class User{
String name;
//创建锁对象
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readlock = lock.readLock(); //读锁
Lock writelock = lock.writeLock(); //写锁
public void setName(String name){ //写过程
try{
writelock.lock();//上写锁writeLock
Thread.sleep(1000);
this.name = name;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writelock.unlock();
}
}
public String getName(){ //读过程
try{
readlock.lock();//上读锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
readlock.unlock();//释放读锁
return name;
}
}
}
1.3 重入锁
重入锁:
- 重入锁也叫作递归锁,指的是同一个线程外层方法获取到一把锁后,内层方法同样具有这把锁的控制权限
- synchronized和Lock锁都可以实现锁的重入
public class ReentrantLock_重入锁_递归锁的实现 {
/**
* 重入锁也叫作递归锁,指的是同一个线程外层方法获取到一把锁后,内层方法同样具有这把锁的控制权限
* synchronized和Lock锁都可以实现锁的重入
* */
public static void main(String[] args) {
new Thread(new MyThread()).start();
}
}
class MyThread implements Runnable{
@Override
public void run() {
a();
}
public synchronized void a(){
System.out.println("a");
b();
}
public synchronized void b(){
System.out.println("b");
c();
}
public synchronized void c(){
System.out.println("c");
}
}
1.4 公平锁
根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。 而非公平锁则在运行时闯入,也就是先来不一定先得 。
ReentrantLock 提供了公平和非公平锁的实现 。
-
公平锁: ReentrantLock pairLock = new ReentrantLock(true)。
-
非公平锁: ReentrantLock pairLock = new ReentrantLock(false)。
如果构造函数不传递参数,则默认是非公平锁 。
例如,假设线程 A 已经持有了锁,这时候线程 B 请求该锁其将会被挂起。当线程 A 释放锁后,假如当前有线程C也需要获取该锁,如果采用非公平锁方式,则根据线程调度策略, 线程B和线程C两者之一可能获取锁,这时候不需要任何其他干涉,而如果使用公平锁则需要把C挂起,让B获取当前锁 。
在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销 。
1.5 Synchronized与Lock
synchronized的缺陷
效率低:锁的释放情况少,只有代码执行完毕或者异常结束会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
无法知道是否成功获得锁,相对而言,Lock可以拿到状态,如果成功获取锁,…,如果获取失败,
Lock解决相应问题
Lock类有以下4个方法:
-
lock(): 加锁
-
unlock(): 解锁
-
tryLock(): 尝试获取锁,返回一个boolean值
-
tryLock(long,TimeUtil): 尝试获取锁,可以设置超时
public class Lock_4Method {
public static void main(String[] args){
//ExecutorService Fixed = Executors.newFixedThreadPool(5);
ExecutorService Cached = Executors.newCachedThreadPool();
MyList myList_Test = new MyList();
//线程一启动
Cached.submit(()->{
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<10;i++){
myList_Test.add(i);
System.out.println(Thread.currentThread().getName()+"添加值!");
}
});
//线程二启动
Cached.submit(()->{
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=10;i<20;i++){
myList_Test.add(i);
System.out.println(Thread.currentThread().getName()+"添加值!");
}
});
//线程三启动
Cached.submit(()->{
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=20;i<30;i++){
myList_Test.add(i);
System.out.println(Thread.currentThread().getName()+"添加值!");
}
});
Cached.shutdown();
while(true){ //判断线程池是否执行完毕!
if(Cached.isTerminated())break;
}
System.out.println(myList_Test.list);
}
}
/**
* Lock类有以下4个方法:
* •lock(): 加锁
* •unlock(): 解锁
* •tryLock(): 尝试获取锁,返回一个boolean值
* •tryLock(long,TimeUtil): 尝试获取锁,可以设置超时
* */
class MyList{
List<Integer> list = new ArrayList();
//创建锁
Lock lock = new ReentrantLock();
public void add(Integer i){
if(lock.tryLock()){ /** tryLock() 尝试获取锁 */
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(i);
lock.unlock();
}else{
System.out.println(Thread.currentThread().getName()+"获取锁失败!");
}
}
}
Synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition与Lock的结合解决了这个问题。
多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的**lockInterruptibly()**方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了
1.6 synchronized锁升级
锁的状态总共有四种
-
无锁 无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。也就是CAS(CAS是基于无锁机制实现的)
-
偏向锁 偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下,则一定会转化为轻量级锁或者重量级锁。
-
**轻量级锁 **当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁优点减少传统的重量级锁使用操作系统互斥量产生的性能消耗靠多次CAS实现的,效率高
-
重量级锁 Synchronized是通过对象内部的一个叫做监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到内核态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。Mutex(**又叫Lock),在多线程中,作为同步的基本类型,**用来保证没有两个线程或进程同时在他们的关键区域
自旋锁
因为挂起线程以及恢复线程要转移到操作系统内核模式执行,这会给性能带来极大的影响。
在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来更改。
自适应锁
在JDK 1.6中引入了。获取锁的自旋次数不确定,根据之前的数据来确认自旋次数或者不自旋,让JVM变得更聪明。
锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。变量是否逃逸,对于虚拟机来说是需要使用复杂的过程间分析才能确定的,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下还要求同步呢?这个问题的答案是:有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中出现的频繁程度也许超过了大部分读者的想象。我们来看看如下例子,这段非常简单的代码仅仅是输出三个字符串相加的结果,无论是源代码字面上,还是程序语义上都没有进行同步。
public String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
String是一个不可变的类,对字符串的连接操作总是通过生成新的String对象来进行的,因此Javac编译器会对String连接做自动优化。在JDK 5之前,字符串加法会转化为StringBuffer对象的连续append()操作,在JDK 5及以后的版本中,会转化为StringBuilder对象的连续append()操作,以上代码会变成如下代码
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer()
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
现在大家还认为这段代码没有涉及同步吗?每个StringBuffer.append()方法中都有一个同步块,锁就是sb对象 。 虚拟机观察变量sb , 经过逃逸分析后会发现它的动态作用域被限制在concatString( ) 方 法内部 。 也就是sb的 所有引用都永远不会逃逸到 concatString( )方法之外 , 其他线程无法访问到它 , 所以这里虽然有锁,但是可以被安全地消除掉。在解释执行时这里仍然会加锁,但在经过服务端编译器的即时编译之后,这段代码就会忽略所有的同步措施而直接执行。
锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据 的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等 待锁的线程也能尽可能快地拿到锁。 大多数情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如上代码连续的append()方法就属于这类情况。如果虚拟机探测到有这样一串零碎的操作 都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,扩展到第一个append()操作之前直至最后一个append()操作之后,这样只需要加锁一次就可 以了。
1.7 使用Synchronized有哪些要注意的?
- 锁对象不能为空,因为锁的信息都保存在对象头里
- 作用域不宜过大,影响程序执行的速度。
- 避免死锁
- 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错
- synchronized是公平锁吗?
synchronized实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,这样有利于提高性能,但是也可能会导致饥饿现象
2、线程安全的集合
2.1 集合结构
2.2 通过Collections获取线程安全集合
Collections工具类中提供了多个可以获得线程安全集合的方法。
方法名 |
---|
public static Collection synchronizedCollection(Collection c) |
public static List synchronizedList(List list) |
public static Set synchronizedSet(Set s) |
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) |
public static SortedSet synchronizedSortedSet(SortedSet s) |
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m) |
2.3 CopyOnWriteArrayList
-
线程安全的ArrayList,加强版读写分离。
-
写有锁,读无锁,读写之间不阻塞,优于读写锁。
-
写入时,先copy一个容器副本、再添加新元素,最后替换引用。
-
使用方式与ArrayList无异。
CopyOnWriteArrayList<String> list=new CopyOnWriteArrayList<>();
2.4 CopyOnWriteArraySet
-
线程安全的Set,底层使用CopyOnWriteArrayList实现。
-
*唯一不同在于,使用**addIfAbsent()*添加元素,会遍历数组。
-
如存在元素,则不添加(扔掉副本)。
2.5 ConcurrentHashMap
JDK1.7实现
-
初始容量默认为16段(Segment),使用分段锁设计。
-
不对整个Map加锁,而是为每个Segment加锁。
-
当多个对象存入同一个Segment时,才需要互斥。
-
最理想状态为16个对象分别存入16个Segment,并行数量16。
-
使用方式与HashMap无异。
JDK1.8实现
-
内部使用CAS交换算法+synchronized实现多线程并发安全
-
CAS有三个值
-
ABA问题
ConcurrentHashMap与HashMap的比较 案例
public class Demo02 {
public static void main(String[] args) {
//HashMap<String,String> map = new HashMap<String, String>();
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>(23);
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
es.submit(new Runnable() {
@Override
public void run() {
//向map集合中添加10个元素
for (int j = 0; j < 10; j++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put("key"+j, "value"+j);
}
}
});
}
es.shutdown();
while(!es.isTerminated()) {}
System.out.println(map);
}
}
2.6 关于CAS
CAS 方式为乐观锁,synchronized 为悲观锁。因此使用 CAS 解决并发问题通常情况下性能更优。
JDK 里面的 Unsafe提供了一个方法
boolean compareAndSwapLong(Object obj,long valueOffset,long expect, long update)
其中compareAndSwap的意思是比较并交换。CAS有四个操作数, 分别为:对 象内存位置 、 对象中 的变量的偏移量 、 变量预期值和新的值 。 其操作含义是 , 如果 对象 obj 中内存偏移量为 valueOffset的变量值为 expect,则使用新的值 update替换 旧的值 expect。 这是处理器提供的一个原子性指令。
CAS只能保证一个共享变量的原子操作
ABA问题:
关于CAS操作有个经典的ABA问题,因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。
ABA 问题 的产生是因为变量 的状态值产生 了环形转换,就是变量的值可 以从A 到 B,然后再从 B 到 A。如果变量的值只能朝着一个方向转换 ,比如 A 到 B, B 到 C, 不构成环形,就不会存在 问题。 JDK 中 的 AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳, 从而避免了ABA问题的产生。