多线程-万字详解

多线程

作者:知否派。

文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢!

一、多线程

1.线程池的意义

线程是稀缺资源,它的创建与销毁是个相对偏重且耗资源的操作,而Java线程依赖于内核线程,创建线程需要进行操作系统状态切换,为避免资源过度消耗需要设法重用线程执行多个任务。

线程池就是一个线程缓存,负责对线程进行统一分配、调优与监控。

什么时候使用线程池?

  • 单个任务处理时间比较短
  • 需要处理的任务数量很大

线程池优势

  • 重用存在的线程,减少线程创建,消亡的开销,提高性能
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性,可统分配, 调优和监控。
2.线程池的五种状态
  • Running 能接受新任务以及处理已添加的任务

  • Shutdown 不接受新任务,可以处理已经添加的任务

  • Stop 不接受新任务,不处理已经添加的任务,并且中断正在处理的任务

  • Tidying 所有的任务已经终止,ctl记录的”任务数量”为0, ctl负责记录线程池的运行状态与活动线程数量

  • Terminated 线程池彻底终止,则线程池转变为terminated状态

img

3.多线程的使用场景
  • 1.后台任务,例如:定时向大量(100w以上) 的用户发送邮件
  • 2.异步处理,例如:统计结果,记录日志发送短信等:
  • 3.分布式计算分片下载、断点续传

小结:

  • 任务量比较大, 通过多线程可以提高效率
  • 需要异步处理时
  • 占用系统资源,造成阻塞的工作时

都可以采用多线程提高效率

4.多线程的创建方式

继承Thread

package com.whcoding.test.thred;

/**
 * @program: spring-boot-learning
 * @description:
 * @author: whcoding
 * @create: 2022-06-10 15:20
 **/
public class MyThreadCreateThread extends Thread {


	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
		}
	}

	/**
	 * 创建2个线程执行数据
	 *
	 * @param args
	 */
	public static void main(String[] args) {
		Thread myThread = new MyThreadCreateThread();
		Thread myThread2 = new MyThreadCreateThread();
		myThread.start();
		myThread2.start();
	}

}

实现Runnable

package com.whcoding.test.thred;

import org.junit.Test;

/**
 * @program: spring-boot-learning
 * @description:Runnable Runnable创建多线程
 * @author: whcoding
 * @create: 2022-06-09 14:35
 **/
public class MyThreadCreateRunnable {

	/**
	 * @param
	 */
	@Test
	public void MyThreadCreateRunnableTest() {

		class MyRunnable implements Runnable {
			private int ticketNumber = 50;

			@Override
			public void run() {

				synchronized (this) {
					for (int i = 0; i < ticketNumber; i++) {
						try {
							Thread.sleep(1000);
							System.out.println(Thread.currentThread().getName() + " loop " + i);
						} catch (InterruptedException e) {
							e.getMessage();
						}
					}
				}

			}
		}

		// 启动3个线程t1,t2,t3(它们共用一个Runnable对象),
		//这3个线程一共卖10张票!这说明它们是共享了MyRunnable接口的。
		Runnable runnable = new MyRunnable();
		Thread t1 = new Thread(runnable, "t1");
		Thread t2 = new Thread(runnable, "t2");
		Thread t3 = new Thread(runnable, "t3");

		t1.start();
		t2.start();
		t3.start();
	}
}

如果希望线程执行完任务之后,给我们一个返回值。此时我们需要执行Callable接口

public static void main(String[] args) {
        FutureTask<Integer> ft = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(ft);
        thread.start();
        try {
            Integer num = ft.get();
            System.out.println("得到的结果:" + num);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
}
 
static class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            int num = 0;
            for (int i = 0; i < 1000; i++) {
                System.out.println("输出" + i);
                num += i;
            }
            return num;
        }
}

线程池创建

package com.whcoding.test.thred;

import org.junit.Test;

import java.util.concurrent.*;

/**
 * @program: spring-boot-learning
 * @description: 多线程创建:线程池
 * @author: whcoding
 * @create: 2022-06-10 14:36
 **/
public class MyThreadCreatePool {

	/**
	 * corePoolSize:线程池中所保存的核心线程数,包括空闲线程
	 * maximumPoolSize:池中允许的最大线程数。
	 * keepAliveTime:线程池中的空闲线程所能持续的最长时间,
	 * 当线程池中的线程数量小于 corePoolSize 时,
	 * 如果里面有线程的空闲时间超过了 keepAliveTime,
	 * 就将其移除线程池,这样,可以动态地调整线程池中线程的数量。
	 * unit:持续时间的单位。
	 * workQueue:任务执行前保存任务的队列,仅保存由 execute 方法提交的 Runnable 任务。
	 **/
	@Test
	public void MyThreadCreatePoolTest(){

		//方法一:不推荐
		//创建带有5个线程的线程池
		//返回的实际上是ExecutorService,而ExecutorService是Executor的子接口
		Executor threadPool = Executors.newFixedThreadPool(5);
		for(int i = 0 ;i < 10 ; i++) {
			threadPool.execute(new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName()+" is running");
				}
			});
		}

		//方法二:
		//创建ThreadPoolExecutor线程池对象,并初始化该对象的各种参数
		//其中线程池工厂参数用于创建线程
		ExecutorService executorService = new
				ThreadPoolExecutor(3,
				5,
				1L,
				TimeUnit.SECONDS,
				new ArrayBlockingQueue<>(3),
				Executors.defaultThreadFactory(),
				new ThreadPoolExecutor.AbortPolicy());

		for (int i = 0; i < 5; i++) {
			executorService.execute(() -> {
				System.out.println(Thread.currentThread().getName() + "===>办理业务");
			});
		}

	}
}

多线程lambda创建

package com.whcoding.test.thred;

/**
 * @program: spring-boot-learning
 * @description: 多线程创建:lambda表达式
 * @author: whcoding
 * @create: 2022-06-10 14:37
 **/
public class MyThreadCreateLambda {
	public static void main(String[] args) {
		//匿名内部类创建多线程
		new Thread() {
			@Override
			public void run() {
				System.out.println(Thread.currentThread().getName() + "mikchen的互联网架构创建新线程1");
			}
		}.start();

		//使用Lambda表达式,实现多线程
		new Thread(() -> {
			System.out.println(Thread.currentThread().getName() + "mikchen的互联网架构创建新线程2");
		}).start();


		//优化Lambda
		new Thread(() -> System.out.println(Thread.currentThread().getName() + "mikchen的互联网架构创建新线程3")).start();

	}
}

5.多线程的停止

使用 interrupt() 以及 isInterrupted()

package com.whcoding.test.thred;

/**
 * @program: spring-boot-learning
 * @description: 多线程的停止
 * @author: whcoding
 * @create: 2022-06-10 17:55
 **/
public class ThreadStop {

	public static void main(String[] args) {
		Thread t1 = new Thread(() -> {
			while (true) {
				try {
					boolean interrupted = Thread.currentThread().isInterrupted();
					if (interrupted) {
						System.out.println("线程已停止");
						break;
					} else {
						System.out.println("线程执行中");
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
		t1.start();
		try {
			Thread.sleep(2000);
			t1.interrupt();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

6. 控制多线程的运行顺序——join方法

面试题:

现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行

Thread的方法join

线程调用了join方法,那么就要一直运行到该线程运行结束,才会运行其他进程.这样可以控制线程执行顺序。

使用join方法,相当于线程的插队方法

package com.whcoding.test.thred;

/**
 * @program: spring-boot-learning
 * @description: 多线程顺序
 * @author: whcoding
 * @create: 2022-06-10 17:56
 **/
public class ThreadSort {

	
	public static void main(String[] args) {
		Thread t1 = new Thread(() -> {
			for (int i = 0; i < 3; i++) {
				System.out.println("t1=====>" + i);
			}
		});

		Thread t2 = new Thread(() -> {
			try {
				t1.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			for (int i = 0; i < 3; i++) {
				System.out.println("t2=====>" + i);
			}
		});

		Thread t3 = new Thread(() -> {
			try {
				t2.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			for (int i = 0; i < 3; i++) {
				System.out.println("t3=====>" + i);
			}
		});
		t1.start();
		t2.start();
		t3.start();
	}
}

7.多线程的生命周期

image-20220610154923918

线程生命周期的阶段描述
新建当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能
阻塞在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态
死亡线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

可以对照上面的线程状态流转图来看具体的方法,这样更清楚具体作用

  1. start()启动当前线程,调用当前线程的run()方法
  2. **run()**通常需要重写Thread类中的此方法, 将创建的线程要执行的操作声明在此方法中

  3. yield() 释放当前CPU的执行权

  4. **join()**在线程a中调用线程b的join(), 此时线程a进入阻塞状态, 知道线程b完全执行完以后, 线程a才结束阻塞状态

  5. sleep(long militime) 让线程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态

  6. wait() 一旦执行此方法,当前线程就会进入阻塞,一旦执行wait()会释放同步监视器。

  7. sleep()和wait()的异同

  • 相同:两个方法一旦执行,都可以让线程进入阻塞状态。
  • 不同:
      1. 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
      2. 调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须在同步代码块中调用。
      3. 关于是否释放同步监视器:如果两个方法都使用在同步代码块呵呵同步方法中,sleep不会释放锁,wait会释放锁。

8.**notify()**一旦执行此方法,将会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先度最高的。

  1. **notifyAll()**一旦执行此方法,就会唤醒所有被wait的线程 。

二、线程池基本概念和使用示例

2.1 基本概念

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。

在开发过程中,合理地使用线程池能够带来3个好处。

1:降低资源消耗

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

2:提高响应速度

当任务到达时,任务可以不需要等到线程创建就能立即执行。

3:提高线程的可管理性

线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

2.2 ExecutorService类创建线程池

/**
	 * 1、corePoolSize 线程池的核心线程数
	 * 2、maximumPoolSize 能容纳的最大线程数
	 * 3、keepAliveTime 空闲线程存活时间
	 * 4、unit 存活的时间单位
	 * 5、workQueue 存放提交但未执行任务的队列
	 * 6、threadFactory 创建线程的工厂类
	 * 7、handler 等待队列满后的拒绝策略
	 */
public void testThread() {
    //其中线程池工厂参数用于创建线程
    ExecutorService executorService = new ThreadPoolExecutor(
        		10, // corePoolSize
                 20,//maximumPoolSize
                 200L,// keepAliveTime
                TimeUnit.SECONDS,// unit
                new ArrayBlockingQueue<>(3),
        		Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    for (int i = 0; i < 5; i++) {
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "===>办理业务");
         });
     }
}
2.3 Executors 类创建线程池

不推荐Executors 类创建线程池

在阿里巴巴java开发手册中明确规定不允许使用Executors创建线程池:

image-20220609161805593

三、Java自带线程池工具

3.1 newCachedThreadPool——不推荐使用

1.底层使用ThreadPoolExector

public static ExecutorService newCachedThreadPool() {
     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

2.特点

没有核心线程,等待队列使用同步队列,出现一个任务就创建一个临时线程去执行任务

3.问题

不会出现内存溢出,但是会浪费CPU资源,导致机器卡死。

image-20220610153734359

3.2 newFixedThreadPool——不推荐使用

1.源码

 public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

2.特点

特定核心线程,无临时线程。等待队列使用链表,等待队列无限长度

3.问题

会导致内存溢出,因为等待队列无限长。

image-20220610153835640

3.3 newSingleThreadExecutor——不推荐使用

1.源码

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

2.特点

创建一个单线程化的线程池, 它只会用唯一的工作线程来执行任务, 保证所有任务按照指定顺序(FIFO,LIFO, 优先级)执行。

只有一个核心线程,依次执行任务。

image-20220610153924528

3.4 newscheduledThreadPool

创建一个定长线程池, 支持定时及周期性任务执行。

3.4.1 延时执行

下面例子是4s之后执行run方法

public static void pool4() {
        ScheduledExecutorService newScheduledThreadPool = 
              Executors.newScheduledThreadPool(5);
        //延时执行的线程池
        //参数:任务  延时时间  时间单位
        newScheduledThreadPool.schedule(new Runnable() {
            public void run() {
                System.out.println("i:" + 1);
            }
        }, 4, TimeUnit.SECONDS);
}
3.4.2 周期性执行任务

下面例子中,设置了一个定时任务,线程开启后,3s后执行任务,每4s执行一次

public static void pool4() {
        ScheduledExecutorService newScheduledThreadPool = 
                Executors.newScheduledThreadPool(5);
        //延时执行的线程池
        //参数:任务  延时时间 间隔时间 时间单位
        newScheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            public void run() {
                System.out.println("i:" + 1);
            }
        }, 3, 4, TimeUnit.SECONDS);
}

四、线程池核心方法

4.1 线程池最基础的框架

image-20220610154150366

public interface Executor {
 
    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}
4.2 ThreadPoolExecutor
4.2.1 ThreadPoolExecutor参数说明
  1. int corePoolSize 核心线程数
  2. int maximumPoolSize 最大线程数
  3. long keepAliveTime, 保持存活的时间——指的是外额线程在没有新任务执行时的存活时间
  4. TimeUnit unit, 时间单位
  5. BlockingQueue workQueue, 任务队列
  6. RejectedExecutionHandler handler 饱和策略
4.2.2 线程池任务与线程的创建顺序

假设ThreadPoolExecutor创建的核心线程数为2,等待队列长度为10,最大线程数为5 .则每个任务来的时候,线程的创建顺序如下:

  • 任务一和任务二来的时候,分别会创建一个核心线程并执行该任务
  • 任务三到十二来的时候,核心线程已满,需要进入等待队列等待
  • 任务十三到十五来的时候,核心线程和等待队列均已满,所以创建额外线程去执行任务
  • 任务十六来的时候,由于整个线程池都已沾满,因此根据饱和策略做出反馈

image-20220610154314317

4.3 线程池的三种队列
4.3.1 SynchronousQueue

synchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。

使用synchronousQueue阻塞队列一般要求maximumRoolsizes为无界,避免线程拒绝执行操作。

  • 当队列中没有任务时,获取任务的动作会被阻塞;
  • 当队列中有任务时,存入任务的动作会被阻塞
4.3.2 LinkedBlockingQueue

LinkedBlockingQueue是个****无界缓存等待队列****。

当前执行的线程数量达到corePoolsize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时max imumPoolsizes就相当于无效了),每个线程完全独立于其他线程。

生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。

4.3.3 ArrayBlockingQueue

ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小

当正在执行的线程数等于corePoolsize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行

当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败, 会开启新的线程去执行

当线程数已经达到最大的maximumPoolsizes时, 再有新的元素尝试加入ArrayBlocki ngQueue时会报错。

4.4 线程池四种拒绝策略

image-20220610154455152

    /* Predefined RejectedExecutionHandlers */
 
    /**
     * A handler for rejected tasks that runs the rejected task
     * directly in the calling thread of the {@code execute} method,
     * unless the executor has been shut down, in which case the task
     * is discarded.
     */
    // 不抛弃任务,请求调用线程池的主线程(比如main),帮忙执行任务
    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }
 
        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }
 
    /**
     * A handler for rejected tasks that throws a
     * {@link RejectedExecutionException}.
     *
     * This is the default handler for {@link ThreadPoolExecutor} and
     * {@link ScheduledThreadPoolExecutor}.
     */
    // 抛出异常,丢弃任务
    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }
 
        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
 
    /**
     * A handler for rejected tasks that silently discards the
     * rejected task.
     */
    // 直接丢弃任务,丢弃等待时间最短的任务
    public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardPolicy}.
         */
        public DiscardPolicy() { }
 
        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }
 
    /**
     * A handler for rejected tasks that discards the oldest unhandled
     * request and then retries {@code execute}, unless the executor
     * is shut down, in which case the task is discarded.
     */
    // 直接丢弃任务,丢弃等待时间最长的任务
    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardOldestPolicy} for the given executor.
         */
        public DiscardOldestPolicy() { }
 
        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }
4.5 关闭线程池
//等待任务队列所有的任务执行完毕后才关闭
executor.shutdown();
//立刻关闭线程池
executor.shutdownNow();
4.6 线程池处理流程

img

五、线程安全

5.1 为什么会出现线程安全问题

Java内存模型(即Java Memory Mode1, 简称JMM)。JMM本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存 (有些地方称为栈空间),用于存储线程私有的数据。

Java内存模型中规定:

所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。

线程对变量的操作(读取赋值等)必须在工作内存中进行——首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝

前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

image-20220610155846326

可以看我之前的文章:

六、Java并发编码三大特性

6.1 原子性
6.1.1 基本概念

原子性,即一个操作或多个操作,要么全部执行并且在执行的过程中不被打断,要么全部不执行。(提供了互斥访问, 在同一时刻只有一个线程进行访问)

可以通过锁的方式解决

6.1.2 代码示例
package com.whcoding.test.thred;

/**
 * @program: spring-boot-learning
 * @description:
 * @author: whcoding
 * @create: 2022-06-10 16:00
 **/
public class ThreadTest {


	static int ticket = 10;

	public static void main(String[] args) {
		Object o = new Object();
		Runnable runnable = () -> {
			while (true) {
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

				//使用synchronized时,需要用一个对象作为锁
				synchronized (o) {
					if (ticket > 0) {
						ticket--;
						System.out.println(Thread.currentThread().getName() +
								"卖了一张票,剩余:" + ticket);
					} else {
						break;
					}
				}
			}
		};
		Thread t1 = new Thread(runnable, "窗口1");
		Thread t2 = new Thread(runnable, "窗口2");
		Thread t3 = new Thread(runnable, "窗口3");
		t1.start();
		t2.start();
		t3.start();
	}
}
6.2 可见性
6.2.1 基本概念

当多个线程访问同一个变量时,-个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

若两个线程在不同的cpu, 那么线程1改变了 i 的值还没刷新到主存,线程2又使用了 i,那么这个 i 值肯定还是之前的,线程1对变量的修改线程没看到。

这就是可见性问题。

6.2.2 可见性问题示例代码
package com.whcoding.test.thred;

/**
 * @program: spring-boot-learning
 * @description:
 * @author: whcoding
 * @create: 2022-06-10 16:00
 **/
public class ThreadTest2 {

	private static boolean flag = true;

	public static void main(String[] args) throws InterruptedException {
		new Thread(() -> {
			System.out.println("1号线程启动,执行while循环");
			long num = 0;
			while (flag) {
				num++;
			}
			System.out.println("1号线程执行,num=" + num);
		}).start();

		Thread.sleep(1);

		new Thread(() -> {
			System.out.println("2号线程启动,更改变量flag值为false");
			setStop();
		}).start();
	}

	public static void setStop() {
		flag = false;
	}
}

6.3 有序性

编译器在执行代码时,可能会对代码进行优化,导致代码执行顺序与预期不符。

七、并发编程之线程间通信

7.1 基本概念
7.1.1 wait、notify

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。

于是我们引出了等待唤醒机制: (wait()、 notify())

wait()、notify()、 notifyA11()是三个定义在object类里的方法,可以用来控制线程的状态。

这三个方法最终调用的都是jvm级的native方法。随着jvm运行平台的不同可能有些许差异。

  • 如果对象调用了wait方法就会使持有该对象的线程把对象的控制权交出,然后处于等待状态。
  • 如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行。
  • 如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。

注意: wait() 方法的调用必须放在synchronized方法或synchronized块中。——因为wait方法的作用是释放锁,所以必须保证有锁

7.1.2 wait和sleep的区别

sleep()方法属于Thread类, wait()方法,属于object类

在调用sleep()方法的过程中,线程不会释放对象锁。

当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持,当指定的时间到了又会自动恢复运行状态。

7.2 实战面试题
7.2.1 使用wait/notify关键字, 两个线程,交替打印1~100

A线程负责打印奇数B线程负责打印偶数。

package com.whcoding.test.thred;

/**
 * @program: spring-boot-learning
 * @description: 交替打印1~100
 * @author: whcoding
 * @create: 2022-06-10 17:00
 **/
public class ThreadTest3 {

	private static int count = 0;
	//当前线程必须拥有此对象的锁,才能调用某个对象的wait()方法能让当前线程阻塞,
	private static final Object lock = new Object();

	public static void main(String[] args) {
		new Thread(new TurningRunner(), "偶数").start();
		new Thread(new TurningRunner(), "奇数").start();
	}

	//拿到锁,我们就打印,一旦打印完唤醒其他线程就休眠
	static class TurningRunner implements Runnable {
		@Override
		public void run() {
			while (count <= 100) {
				synchronized (lock) {
					System.out.println(Thread.currentThread().getName() + ":" + count++);
					lock.notify();
					if (count <= 100) {
						try {
							//如果任务没结束,唤醒其他线程,自己休眠
							lock.wait();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}
		}
	}
}

7.2.2 使用synchronized关键字

创建两个线程,一个线程处理偶数,一个线程处理奇数,两个线程之间通过synchronized进行同步,保证count++每次只有一个线程进行操作

为什么两个线程能交替执行,这里很巧的是count从0123…自增过程就是一个奇偶数交替的过程,实际上两个线程都是在不停的尝试(while循环)进入synchronized代码块,如果满足相对应的条件(偶数或是奇数)就打印输出。

package com.whcoding.test.thred;

/**
 * @program: spring-boot-learning
 * @description:
 * @author: whcoding
 * @create: 2022-06-10 17:07
 **/


/**
 * 两个线程交替打印0~100的奇偶数,用synchroized关键字实现
 * <p>
 * 这种写法有很多多余操作.就是同一个线程会出现多次抢到了锁,但是不满足条件跳过了if语句并不会执行
 * count++,这种写法效率并不高效
 */
public class SynchroizedPrintOddEven {
	private static int count;
	//临界资源
	private static final Object lock = new Object();
	//新建两个线程,第1个只处理偶数,第二个只处理奇数(用位运算),用synchronized来通讯

	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				while (count < 100) {
					synchronized (lock) {
						//count & 1 一个数字把它和1做位与的操作,1再二进制就是1,count转换位二进制,和1去与,就是取出count二进制的最低位,最低位是1代表奇数,0代表是偶数,比count%2 == 0 效率高
						//因为线程是随机抢锁的,可能会出现同一个线程多次进入,但是不满足条件,并不会执行count++.
						if ((count & 1) == 0) {
							System.out.println(Thread.currentThread().getName() + ":" + count++);
						}
					}
				}
			}
		}, "偶数").start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				while (count < 100) {
					synchronized (lock) {
						//count & 1 一个数字把它和1做位与的操作,1再二进制就是1,count转换位二进制,和1去与,就是取出count二进制的最低位,最低位是1代表奇数,0代表是偶数,比count%2 == 0 效率高
						if ((count & 1) == 1) {
							System.out.println(Thread.currentThread().getName() + ":" + count++);
						}
					}
				}
			}
		}, "奇数").start();
	}
}

八、volatile、synchronized、Lock

8.1 volatile关键字
8.1.1 volatile关键字的作用

1、保证可见性;
2、防止指令重排;
3、但是不保证原子性

可见性是什么

在JMM(java memory model)java内存模型中,其他线程从主内存空间把值拷贝到自己的工作空间,线程修改之后的值会返回给主内存,主内存会通知其他线程,此为可见性。

指令重排

  • CPU为了执行效率会并发执行操作指令,volatile可以使指令一个一个的执行。

如何解决原子性

  • 通过synchronized关键字。
  • 通过使用AtomicXX,不加锁,采用CAS(compareAndSet)解决。其本质是使用UnSafe本地方法(CPU原语)。
  • 使用LongAdder:最快(在线程多的情况下,使用分段锁)
8.2 synchronized关键字
8.2.1 基本概念
  • 可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块
  • synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile

synchronized必须使用一个对象作为锁

8.2.2 synchronized的基本原理

锁由jvm帮忙实现。

JVM是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。

具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。

其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit 之后才能尝试继续获取锁。
image-20220610160454018

image-20220610160504038

8.2.3 synchronized同步锁的释放时间
  1. synchronized不需要手动释放锁,底层会自动释放,Lock则需要手动释放锁,否则有可能导致死锁。
  2. synchronized等待不可中断,除非抛出异常或者执行完成【同步代码块中遇到break 、 return 终于该代码块或者方法的时候释放】, Lock可以中断,通过interrupt()可中断。
  3. 出现未处理的error或者exception导致异常结束的时候释放。
  4. 程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁

如下情况不会释放锁

  1. 程序调用 Thread.sleep() Thread.yield() 这些方法暂停线程的执行,不会释放。
  2. 线程执行同步代码块时,其他线程调用 suspend 方法将该线程挂起,该线程不会释放锁 ,所以我们应该避免使用 suspend 和 resume 来控制线程
  3. lock可以主动去释放锁,而synchronized是被动的

image-20220609170901886

8.2.4 线程的同步

线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏,线程的同步是保证多线程安全访问竞争资源的一种手段。

1.普通同步方法

锁是当前实例对象 ,进入同步代码前要获得当前实例的锁。

/**
	 * 用在普通方法
	 */
	private synchronized void synchronizedMethod() {
		System.out.println("--synchronizedMethod start--");
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("--synchronizedMethod end--");
	}

2.静态同步方法

锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁。

/**
	 * 用在静态方法
	 */
	private synchronized static void synchronizedStaticMethod() {
		System.out.println("synchronizedStaticMethod start");
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("synchronizedStaticMethod end");
	}

3.同步方法块

锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

/**
	 * 用在类
	 */
	private void synchronizedClass() {
		synchronized (SynchronizedTest.class) {
			System.out.println("synchronizedClass start");
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("synchronizedClass end");
		}
	}
8.3 Lock

在jdk1.5之后,并发包中新增了Lock 接口(以及相关实现类)用来实现锁功能,Lock 接口提供了与synchronized关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。

8.3.1 Lock锁的用法
Lock lock = new ReentrantLock();
lock.lock();
try {
 
     //可能会出现线程安全的操作
 
} finally {
 
     //- 定在finally中释放锁
     //也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
     lock.unlock();
 
}
8.3.2 lock比synchronized更优的地方

1. 支持tryLock尝试获取锁

lock有tryLock接口。当锁被占用时,其他线程在tryLock失败后,无需等待,可以去做别的事情

而synchronized必须等待锁释放

2. 支持读写锁

8.3.3 lock和synchronized的区别
  1. Lock是一 个接口,而synchroni zed是Java中的关键字

synchroni zed是内置的语言实现;synchronized关键字可以直接修饰方法,也可以修饰代码块,而lock只能修饰代码块

  1. synchronized在发生异常时,会自动释放线程占有的锁

因此不会导致死锁现象发生; 而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁

  1. Lock可以让等待锁的线程响应中断,而synchronized却不行

使用synchronized时,等待的线程会直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。(提供tryLock)

  1. Lock可以提高多个线程进行读操作的效率。(提供读写锁)

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 lock的性能要远远优于synchronized.

所以说,在具体使用时要根据适当情况选择。

九、如何合理设置线程池大小

如何合理设置线程池大小:https://www.cnblogs.com/xiang–liu/p/9710096.html

要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析:

  1. 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。
  2. 任务的优先级:高、中、低。
  3. 任务的执行时间:长、中、短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接等。

性质不同的任务可以交给不同规模的线程池执行。

对于不同性质的任务来说,CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数,IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1,而对于混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。

若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。
当然具体合理线程池值大小,需要结合系统实际情况,在大量的尝试下比较才能得出,以上只是前人总结的规律。

在这篇如何合理地估算线程池大小?文章中发现了一个估算合理值的公式

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

可以得出一个结论:
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
以上公式与之前的CPU和IO密集型任务设置线程数基本吻合。

参考

Java多线程超级详解:https://blog.csdn.net/m0_67698950/article/details/123722760

多线程以及线程池:https://blog.csdn.net/qq_29996285/article/details/118955325

如何合理设置线程池大小:https://www.cnblogs.com/xiang–liu/p/9710096.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值