[JAVA]-多线程
[JAVA]-多线程
[TOC]
1.线程的基本概念
【1】基础概念
单线程 VS 多线程
单线程编程:顺序执行流,程序从main方法开始执行,依次从上往下执行,如果遇到了阻塞程序将会在该处停滞。
但是实际情况单线程的程序功能有限的,例如在开发一个简单的服务器的时候,需要通过这个服务器程序向不同的客户端提供不同的服务,而不同的客户端直接应该互不干扰 ,这个时候便需要通过多线程解决问题
并发 VS 并行
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
【2】线程和进程
📌进程VS线程
(1)进程
几乎所有的操作系统都支持进程的概念,所有运行的任务通常对应一个进程,当一个程序进入到内存中的是既变为一个进程。
进程:处于运行中的程序,并且具有一定的独立功能,进程是系统进程资源分配和调度的一个独立单位
独立性:一个能独立运行的基本单位,是系统分配资源和调度的独立单位
动态性:进程的实质是程序的一次执行过程,进程是动态产生、动态消亡的
并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响
(2)线程
线程:是进程中的单个顺序控制流,是一条执行路径
单线程:一个进程如果只有一条执行路径,则称为单线程程序
多线程:一个进程如果有多条执行路径,则称为多线程程序
线程的优势
进程之间不能共享内存,但线程之间共享内存非常容易
创建进程重新分配资源,但是创建线程代价非常小、暂用内存比较小,因此使用多线程实现并发比多进程效果更高
Java内置多线程的支持,不是单纯的调用,而是通过操作系统的调度,简化了java多线程的编程
(3)程序、进程、线程
进程VS程序:
程序只是一个静态指令的集合,进程是一个正在系统中活动的指令集合,在进程中加入了时间的概念,进程具有了生命周期和各种不同的状态,这些都是程序不具备的
进程VS线程:
线程是程序中独立的并发执行流,进程是包含了多个线程,进程中的线程之间的隔离度要小,可以相互访问,他们是共享内存的文件句柄和其他每个进程应用的状态
2.线程的创建和生命周期
【1】线程的创建
🔖创建线程
线程的创建有三种方式:继承Thread、实现Runnable、实现Callable接口
- 继承Thread类,重写run方法,调用start方法启动线程
class MyThread1 extends Thread {
/**
* 线程创建方式1:
* a.继承Thread类
* b.重写相应的run方法
* c.调用start方法启动相应的线程
*/
@Override
public void run() {
while(true){
System.out.println("方式1创建线程......");
}
}
}
public class CreateThreadDemo1 {
public static void main(String[] args) {
// 方式1:继承Thread类(此类线程启动直接调用start方法)
// 通过new关键字创建线程,此时线程ct1处于“新建new”状态
MyThread1 mt = new MyThread1();
// 线程ct1调用start方法,此时线程不是直接运行,而是处于“就绪ready”状态
mt.start();
}
}
- 实现Runnable接口,重写run方法,调用start方法启动线程
class MyThread2 implements Runnable {
/**
* 线程创建方式2:
* a.实现Runnable接口
* b.重写run方法
* c.调用start方法启动线程(需要通过Thread进行封装)
*/
@Override
public void run() {
while (true) {
System.out.println("方式2创建线程......");
}
}
}
public class CreateThreadDemo2 {
public static void main(String[] args) {
// 方式2:实现Runnable接口(此类线程不能直接调用start方法,需要通过Thread类进行封装再调用start方法启动线程)
MyThread2 mt = new MyThread2();
Thread thread = new Thread(mt);
thread.start();
}
}
- 实现Callable接口(从任务中产生返回值)
class MyThread3 implements Callable<String> {
/**
* 线程创建方式3:基于Callable
* a.实现Callable接口
* b.重写call方法
* c.借助FutureTask对象创建间Thread对象,随后启动线程
* d.调用get方法获取线程结束之后的结果
*/
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println("方式3:借助Callable创建线程" + i);
}
return "call线程";
}
}
public class CreateThreadDemo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 线程开启之后需要执行里面的call方法
MyThread3 mt = new MyThread3();
// 构建FutureTask对象
FutureTask<String> ft = new FutureTask<>(mt);
//创建线程对象
Thread t = new Thread(ft);
// 开启线程
t.start();
System.out.println(ft.get());
}
}
测试类
当线程调用start方法之后会进入就绪ready状态(而非运行run状态),线程内部依赖jvm的调度,当线程调用start方法之后,jvm会判定这个线程是否可执行,但至于什么时候执行则取决于jvm内部的调度,由CPU自行决定要指定的线程
public class ThreadTest {
/**
* 1>分别以两种方式测试线程创建,分析其生命周期
* a.继承Thread类
* b.实现Runnable接口
*
* 2>线程的5个状态分析
* 新建 new
* 就绪 ready
* 运行 run
* 阻塞 wait
* 死亡 terminal(结束)
*/
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 方式1创建线程:继承Thread
MyThread1 mt = new MyThread1();
mt.start();
// 方式2创建线程:实现Runnable接口(可通过Lambda表达式创建并启动线程)
new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("方式2创建线程:借助Lambda表达式构建");
}
}
}).start();
/**
* 执行结果分析:线程的启动主要CPU内部的调度(涉及CPU时间片的概念)
* 此处执行的结果显示,每次执行的线程顺序均有所不同,
* 且每个线程是交替执行的,一个线程执行一段时间之后便将CPU交付给下一个线程执行以此类推
*/
}
}
🔖线程设置
常用的线程方法
方法名 | 说明 |
---|---|
void setName(String name) | 设置线程名称 |
String getName() | 返回线程名称 |
Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
【2】不同创建方式的对比
线程有几种创建方式? 这几种创建方式有和优缺点有何区别?
采用实现Runnable接口的方式
线程仅仅是实现了Runnable接口,还可以继承其他类
在这种方式下,可以多个线程共享一个Target对象,非常适合多个线程共同处理同一份资源,从而将cpu代码、数据分开,较好的体现了面向对象的思想
劣势:编程稍微复杂,如果要访问当前线程,必须使用Thread.currentThread()方法
采用继承Thread类的方式
劣势:因为线程继承了Thread类,所有不能再继承其他父类
优势:编程比较简单,如果需要访问当前线程,直接使用this对象即可
采用实现Callable接口的方式
劣势:编程相对复杂,不能直接使用Thread类中的方法(需借助第三方对象进行构建)
优势:扩展性强,实现该接口的同时还可继承其他的类
【3】线程的生命周期
🔖线程的生命周期概念
当线程创建并启动后 它经历五种状态:新建、就绪、运行、阻塞、死亡
新建
当线程对象创建出来进入了新建状态,和其他java对象一样,仅仅是由java虚拟机为其分配了内存
就绪
当调用start方法之后进入就绪状态。这里start后不是进入运行状态 其实线程内部还是依赖jvm的调度 当调用start方法之后 jvm会认为这个线程可以执行的 至于什么执行取决于jvm内部的调度
运行
如果就绪状态的线程获得了cpu的执行权,这个线程就是运行状态,当这个线程运行的时候不会一直霸占cpu的执行权,线程在执行的过程中 会从cpu上调度下来 以便其他线程能获取执行的机会
阻塞(线程进入停滞状态 ?)
在以下情况下线程会由运行进行阻塞状态:
线程调用一个阻塞的方法 方法返回该线程之前 该线程一直阻塞
线程调用sleep方法
线程尝试获取同步监视器,但是该同步监视器被其他线程持有
线程调用suspend方法挂起
由阻塞进入运行状态?
调用阻塞的方法返回
调用sleep方法到期
线程成功获取同步监视器
调用resume方法恢复
死亡
"死亡"就是线程的结束
让线程结束的方法:
Run方法执行完毕
线程抛出异常
直接调用stop方法结束线程
判断线程死亡可以使用isAlive
方法。当线程处于就绪、运行和阻塞状态返回true,否则返回false
不能对死亡的线程重写调用start方法重新启动,另外suspend 、stop方法容易导致死锁,不推荐使用
📌JAVA中的线程状态
Java中的线程状态在java.lang.Thread.State中有相应定义,源码参考如下所示
public class Thread {
public enum State {
/* 新建 */
NEW ,
/* 可运行状态 */
RUNNABLE ,
/* 阻塞状态 */
BLOCKED ,
/* 无限等待状态 */
WAITING ,
/* 计时等待 */
TIMED_WAITING ,
/* 终止 */
TERMINATED;
}
// 获取当前线程的状态
public State getState() {
return jdk.internal.misc.VM.toThreadState(threadStatus);
}
}
线程状态 | 具体含义 |
---|---|
NEW | 【初始状态|开始状态】一个尚未启动的线程的状态。线程刚被创建,但是还没调用start方法,并未启动。MyThread t = new MyThread()只有线程象,没有线程特征 |
RUNNABLE | 当调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,这个中间状态被称为可执行状态(RUNNABLE)即其具备执行的资格,但是并没有真正的执行起来而是在等待CPU调度 |
BLOCKED | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态; 当该线程持有锁时,该线程将变成Runnable状态 |
WAITING | 【等待状态】一个正在等待的线程的状态,处于等待状态的线程正在等待其他线程去执行一个特定的操作 造成线程等待的原因有两种:分别为调用Object.wait()、join()方法 |
TIMED_WAITING | 【限时等待状态】一个在限定时间内等待的线程的状态。 造成线程限时等待状态的原因有三种:分别为调用Thread.sleep(long),Object.wait(long)、join(long)。 |
TERMINATED | 一个完全运行完成的线程的状态。也称之为终止状态、结束状态 |
线程状态图
案例1:【计时等待】状态转化
NEW -> RUNNABLE -> TIME_WAITING -> RUNNABLE -> TERMINATED
public class ThreadStateDemo1 {
public static void main(String[] args) throws InterruptedException {
// 定义一个内部线程
Thread thread = new Thread(() -> {
System.out.println("2.执行thread.start()之后:" + Thread.currentThread().getState());
try {
// 休眠100毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("4.执行Thread.sleep(long)完成之后:" + Thread.currentThread().getState());
});
// 1.获取start()之前的状态
System.out.println("1.通过new初始化一个线程,调用start()之前:" + thread.getState());
// 2.启动线程
thread.start();
// 3.休眠50毫秒(因为thread需要休眠100毫秒,所以在第50毫秒,thread处于sleep状态)
Thread.sleep(50);
System.out.println("3.执行Thread.sleep(long)时:" + thread.getState());
// thread和main线程主动休眠150毫秒,所以在第150毫秒,thread早已执行完毕
Thread.sleep(100);
System.out.println("5.线程执行完毕之后:" + thread.getState() + "\n");
}
}
案例2:【等待】状态转化
NEW -> RUNNABLE -> WAITING -> RUNNABLE -> TERMINATED
public class ThreadStateDemo2 {
public static void main(String[] args) throws InterruptedException {
// 定义一个对象,用来加锁和解锁
Object obj = new Object();
// 定义一个内部线程
Thread thread = new Thread(() -> {
System.out.println("2.执行thread.start()之后:" + Thread.currentThread().getState());
synchronized (obj) {
try {
//thread需要休眠100毫秒
Thread.sleep(100);
//thread100毫秒之后,通过wait()方法释放obj对象锁
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("4.被object.notify()方法唤醒之后:" + Thread.currentThread().getState());
});
// 1.获取start()之前的状态
System.out.println("1.通过new初始化一个线程,执行start()之前:" + thread.getState());
// 2.启动线程
thread.start();
// 3.main线程休眠150毫秒(因为thread在第100毫秒进入wait等待状态,所以第150秒肯定可以获取其状态)
Thread.sleep(150);
System.out.println("3.执行object.wait()时:" + thread.getState());
// 定义另一个线程进行解锁
new Thread(() -> {
synchronized (obj) {
// 唤醒等待的线程
obj.notify();
}
}).start();
// main线程休眠10毫秒等待thread线程能够苏醒
Thread.sleep(10);
// 获取thread运行结束之后的状态
System.out.println("5.线程执行完毕之后:" + thread.getState() + "\n");
}
}
案例3:【阻塞】状态转化
NEW -> RUNNABLE -> BLOCKED -> RUNNABLE -> TERMINATED
public class ThreadStateDemo3 {
public static void main(String[] args) throws InterruptedException {
// 定义一个对象,用来加锁和解锁
Object obj = new Object();
// 定义一个线程,先抢占了obj对象的锁
new Thread(() -> {
synchronized (obj) {
try {
Thread.sleep(100); // 第一个线程持有锁100毫秒
obj.wait(); // 通过wait()方法进行等待状态,并释放obj的对象锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 定义目标线程,获取等待获取obj的锁
Thread thread = new Thread(() -> {
System.out.println("2.执行thread.start()之后:" + Thread.currentThread().getState());
synchronized (obj) {
try {
Thread.sleep(100); // thread要持有对象锁100毫秒
obj.notify();// 通过notify()方法唤醒所有在obj上等待的线程继续执行后续操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("4.阻塞结束后:" + Thread.currentThread().getState());
});
// 1.获取start()之前的状态
System.out.println("1.通过new初始化一个线程,执行thread.start()之前:" + thread.getState());
// 2.启动线程
thread.start();
// 3.先等100毫秒(第一个线程释放锁至少需要100毫秒,所以在第50毫秒时,thread正在因等待obj的对象锁而阻塞)
Thread.sleep(50);
System.out.println("3.因为等待锁而阻塞时:" + thread.getState());
//再等300毫秒(两个线程的执行时间加上之前等待的50毫秒总共是250毫秒,所以在第300毫秒,所有的线程都已经执行完毕)
Thread.sleep(300);
System.out.println("5.线程执行完毕之后:" + thread.getState());
}
}
3.线程控制和线程安全
【1】线程控制
🔖join线程
假设有A线程和B线程,在执行A线程的时候,需要依赖线程B。需要A线程在执行的过程中进行阻塞,等待线程B的执行完毕再次执行线程A(排队加塞)
public class JointThreadDemo {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.setName("主线程....");
mt.start();
/**
* 由结果显示,当主线程执行到i==30时,加塞线程进入
* 直到执行完加塞线程才执行主线程的下一步操作
*/
}
}
//自定义线程
class MyThread extends Thread
{
@Override
public void run() {
for(int i=0;i<50;i++)
{
if(i==30)
{
//当i==30满足时安排加塞线程
Thread t = new Thread(new JointThread());
t.setName("加塞线程....");
/**
* 在创建自定义的加塞线程之后,先调用start方法将
* 当前的线程加塞线程加入就绪队列(处于就绪状态),随后
* 调用join方法实现“加塞”
* 必须是先start后join,否则出错
*/
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.getName()+"执行"+i);
}
}
}
//定义加塞线程
class JointThread extends Thread
{
@Override
public void run() {
for(int i=0;i<50;i++)
{
/**
* 获取当前线程的名称
* 1.先通过Thread.currentThread()获取当前的线程,
* 再调用getName()方法获取线程名称
* Thread.currentThread().getName();
* 2.用this关键字指代当前线程
* this.getName();
*/
System.out.println(Thread.currentThread().getName()+"执行"+i);
}
}
}
🔖线程休眠
使用Thread的静态方法 sleep方法让正在执行的线程进入休眠,在休眠的过程中不会放弃cpu的执行权,等休眠时间到了马上获得cpu的执行权。(在sleep的过程中其他线程不会获得cpu的执行权)
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
(1)示例分析
/**
* 线程休眠:
* 让当前线程休眠一定的时间,但线程始终不会放弃CPU的执行权,
* 等待休眠结束后,继续获得CPU的执行权,且在sleep休眠期间
* 其它线程不会获得CPU的执行权
*/
public class ThreadSleepDemo {
public static void main(String[] args) {
new Thread(new Runnable(){
@Override
public void run() {
for(int i=0;i<100;i++)
{
// 实现每打印一个随机数据休眠一秒
try {
System.out.println((int)Math.random()*100);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
(2)自定义时钟
public class Clock {
public static JFrame jf = new JFrame();
public static JLabel clock = new JLabel();
public static void init()
{
new Thread(new Runnable(){
@Override
public void run() {
while(true)
{
clock.setText("当前系统时间:"+getTime());
System.out.println("当前系统时间:"+getTime());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
jf.setBounds(300, 200, 300, 100);
jf.setVisible(true);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
clock.setHorizontalAlignment(JLabel.CENTER);
jf.add(clock);
}
public static String getTime()
{
Calendar c = Calendar.getInstance();
String time = c.get(Calendar.YEAR)+"-"
+c.get(Calendar.MONTH)+"-"
+c.get(Calendar.DATE)+" ";
//设定指定的格式
int hour = c.get(Calendar.HOUR_OF_DAY);
int minute = c.get(Calendar.MINUTE);
int second = c.get(Calendar.SECOND);
String ph = hour>10 ? "" : "0";
String pm = hour>10 ? "" : "0";
String ps = hour>10 ? "" : "0";
time += ph+hour+":"+pm+minute+":"+ps+second;
return time;
}
public static void main(String[] args) {
new Clock().init();
}
}
🔖线程让步
线程的让步和线程的休眠功能相似,但是有一个最重要的区别,线程的休眠是让当前线程进入休眠,但是不会放弃cpu的执行权。但是线程的让步不同,是会放弃cpu的执行权,让操作系统重新调度,其他线程也能有相同的几率获得cpu的执行权
(1)线程的让步yield
此处需注意的是yield()只是一个让步暗示,并没有任何一个机制确保它一定会被采纳,也就是说对于任何重要的控制或者调整应用时不能单纯地依赖yield()
/**
* 线程让步:
* 线程的让步会放弃cpu的执行权,让操作系统重新调度,其他线程也能有相同的几率获得cpu的执行权
*/
public class ThreadYieldDemo extends Thread{
//在创建线程的时候直接指定线程的名称
public ThreadYieldDemo(String name)
{
super(name);
}
@Override
public void run() {
for(int i=0;i<50;i++)
{
if(i==30)
{
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"执行:"+i);
}
}
public static void main(String[] args) {
ThreadYieldDemo ty1 = new ThreadYieldDemo("线程001");
ThreadYieldDemo ty2 = new ThreadYieldDemo("线程002");
ty1.start();
ty2.start();
/**
* 结果分析:
* 每次测试的结果均不相同,原因是当每个线程执行到i==30是会执行相应的yield方法
* 即实现线程“让步”,放弃当前cpu执行权,使得每个线程都拥有相同的机会抢占cpu的
* 执行权,因此每次输出的结果都不尽相同(由cpu自行决定要执行哪个线程)
* 可能是“线程001”先执行结束,也可能是“线程002”先执行结束
*/
}
}
🔖线程优先级
线程调度:Java采用的是抢占式调度模型
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
每个线程都具备一个优先级,优先级越高越容易获取执行的机会,优先级越低执行的机会越少,默认都是普通的优先级
❓为什么多线程程序的执行具有随机性?
假如计算机只有一个 CPU, CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片(即使用权),才可以执行指令。因此多线程程序的执行是有随机性的,因为谁抢到CPU的使用权是不一定的
注意:并非一定优先级高的一定先执行,也有可能优先级高的在和优先级低的竞争的过程中优先级低的获取cpu的执行权
常用方法:
方法名 | 说明 |
---|---|
final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改此线程的优先级线程默认优先级是5; 线程优先级的范围是:1-10 |
public class ThreadPriortyDemo extends Thread{
public ThreadPriortyDemo(String name)
{
super(name);
}
@Override
public void run() {
for(int i=0;i<50;i++)
{
if(i==20)
{
// 线程让步,让其他线程拥有获取cpu的机会
Thread.yield();
}
System.out.println(this.getName()+"执行"+i);
}
}
public static void main(String[] args) {
/**
* 分别创建两个线程,设置不同的优先级
* 通过setPriority方法设置不同的优先级(从1-10)
* 执行两个线程,查看分析运行结果
*/
ThreadPriortyDemo tp1 = new ThreadPriortyDemo("高级线程...");
ThreadPriortyDemo tp2 = new ThreadPriortyDemo("低级线程...");
tp1.setPriority(2);
tp2.setPriority(1);
/**
* tp1.setPriority(MAX_PRIORITY);
* tp2.setPriority(MIN_PRIORITY);
* NORM_PRIORITY
*/
tp1.start();
tp2.start();
/**
* 由测试结果分析,虽然线程的优先级不同,但最终先执行结束的线程始终不确定,
* 有可能是低级线程先执行结束,也有可能是高级线程先执行结束,但可以看到的是
* 优先级较高的高级线程拥有比较大的几率抢占到cpu,因此优先级较高的线程先执行
* 结束的可能性较大,但并不是一定.
*/
}
}
🔖守护线程
后台线程(守护线程),是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此当所有的非后台线程结束时,程序也就终止了,同时会杀死所有后台线程。反过来说,只要有任何非后台线程(用户线程)还在运行,程序就不会终止。
The Java Virtual Machine exits when the only threads running are all daemon threads.
当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出
方法名 | 说明 |
---|---|
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
参考案例
class CommonThread implements Runnable {
@Override
public void run() {
try {
while (true) {
Thread.sleep(1000);
System.out.println("#" + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 如果是后台线程(守护线程)则不执行finally语句
System.out.println("finally语句执行");
}
}
}
public class SimpleDaemonDemo {
public static void main(String[] args) throws InterruptedException {
// 设置一个钩子线程,在JVM退出的时候输出日志
Runtime.getRuntime().addShutdownHook(new Thread(()->{
System.out.println("JVM exit ......");
}));
for (int i = 0; i < 10; i++) {
Thread daemon = new Thread(new CommonThread());
// Thread.sleep(1000);
// 需在线程start之前调用setDaemon方法设置后台线程
daemon.setDaemon(true);
daemon.start();
}
System.out.println("All daemons started");
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果分析
上述结果运行可看到部分线程被创建的结果,可通过设定sleep的时间查看线程创建的效果,当main方法执行结束(主线程退出),程序中没有要执行的非守护线程,因此JVM也会退出(通过设定钩子线程,在JVM退出的时候输出日志)。
调整sleep方法的时间,可看到线程创建的不同效果(在main还没结束的时候守护线程仍然正常构建)
如果是非守护线程(取消setDaemon),可以看到线程在不断地被构建,且JVM始终保持运行状态
也就是说,守护线程拥有结束自己生命周期的特性,守护线程一般用于执行一些后台任务,在程序退出或者JVM退出的时候线程自动关闭
【2】线程安全
<<Java并发编程实践>> 作者定义的线程安全:
如果多个线程访问同一个类,如果不考虑这些线程在运行环境下的调度和交替执行,并且不需要额外的同步以及在调用方法代码不必做其他协调,那么这类的行为仍然是正确的,这个类是线程安全的
/**
* 创建一个公共类其中提供一个方法供外界资源访问
*/
class ShowString
{
public void show(String str)
{
for(int i = 0;i<str.length();i++)
{
System.out.print(str.charAt(i));
}
System.out.println();
}
}
public class ThreadSafeTest {
/**
* 定义公共类的对象(保证要测试的三个线程访问的是同一个对象)
* 开启三个测试线程,访问定义的对象
*/
ShowString s = new ShowString();
public void init()
{
new Thread(new Runnable(){
@Override
public void run() {
while(true)
{
s.show("线程AAAAAAAAAA");
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
while(true)
{
s.show("线程BBBBBBBBBB");
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
while(true)
{
s.show("线程CCCCCCCCCC");
}
}
}).start();
}
public static void main(String[] args) {
ThreadSafeTest test = new ThreadSafeTest();
test.init();
}
/**
* 结果部分显示:
* 线程BBBBAAAAAAAAAA
* 线程ABBBBBB
* 线程BBBBBBBBBB
* 线程BBB线程CCCCCCCCCC
* 线程CCCCCCCCCC
* 如果是在上述情况下可能会出现线程A、B、C打印结果是混淆在一起的,
* 并没有得到理想的结果,说明ShowString这个类并不是一个线程安全的
* 如果要实现要么打印A,要么打印B,要么打印C,而不相互影响,相互混淆
* 则可以通过相应的“锁”实现
*/
}
同步代码块
class ShowString
{
/**
* 锁:关键字synchronized
* synchronized是一个重量级的锁,当前锁的含义是锁住一个方法
* 即锁住当前方法,使得一次只能有一个线程进入到当前方法内部
*/
public synchronized void show(String str)
{
for(int i = 0;i<str.length();i++)
{
System.out.print(str.charAt(i));
}
System.out.println();
}
}
同步代码块(同步监视器)
就是定义一组原子操作,在这个原子代码块中一次只允许一个线程进入,只有当前线程运行结束才能允许下一个线程进入
/**
* 同步代码块(同步监视器)
* 1.对象锁:this指代当前对象
* 2.类锁:类名.class
* 3.任意对象锁:Object对象
* 4.锁字符串常量:
* 区分new String("haha")、"haha"
*/
Synchronized(obj)
{
//被锁定的代码
}
# 其中obj可以理解为一把锁,也可以理解为同步监视器
(1)对象锁:this当前对象
public void show(String str)
{
synchronized (this) {
for(int i = 0;i<str.length();i++)
{
System.out.print(str.charAt(i));
}
System.out.println();
}
}
(2)类锁:锁类的字节码
public void show(String str)
{
synchronized (ShowString.class) {
for(int i = 0;i<str.length();i++)
{
System.out.print(str.charAt(i));
}
System.out.println();
}
}
(3)任意对象锁
//obj要定义在方法体外,否则失效
Object obj = new Object();
public void show(String str)
{
synchronized (obj) {
for(int i = 0;i<str.length();i++)
{
System.out.print(str.charAt(i));
}
System.out.println();
}
}
(4)锁字符串常量
public void show(String str)
{
/**
* "xx":显示正常
* new String("xx"):无效锁定
*/
synchronized ("xx") {
for(int i = 0;i<str.length();i++)
{
System.out.print(str.charAt(i));
}
System.out.println();
}
}
📌案例分析
(1)案例1:多线程实现自加
案例需求
同时执行三个线程 ,每个线程自加10000,然后得到总结果三万
实现步骤
1.创建公共的count属性以及add方法供线程访问(考虑线程安全问题)
2.创建多个线程用以访问add方法
3.通过查看线程活跃数阻塞主方法,使得当上述构建的线程都执行完毕之后再放行,避免子线程还没结束就提前结束主方法
参考代码
public class ThreadSafeDemo1 {
public int count = 0;
/**
* 1.创建公共类,提供自加方法,供三个线程访问
*/
class Demo
{
/**
* 三个线程访问同一个对象,要确保线程安全问题,因此该方法用
* synchronized关键字加锁,从而使得该方法一次只能允许一个线程进入
*/
public synchronized void add()
{
count++;
}
}
/**
* 分别创建三个线程用以访问Demo类中的add方法
*/
Demo demo = new Demo();
public void init()
{
// 线程1
new Thread(new Runnable(){
@Override
public void run() {
for(int i=0;i<10000;i++){
demo.add();
}
}
}).start();
// 线程2
new Thread(new Runnable(){
@Override
public void run() {
for(int i=0;i<10000;i++){
demo.add();
}
}
}).start();
// 线程3
new Thread(new Runnable(){
@Override
public void run() {
for(int i=0;i<10000;i++){
demo.add();
}
}
}).start();
while(Thread.activeCount()!=1)
{
/**
* 阻塞,当上方分三个线程都执行结束之后再执行主线程,
* 否则当执行还没有达到30000便执行主线程,提前结束操作
* 则不会得到预期的30000(且每次执行的结果显示都不尽相同)
*/
System.out.println("主线程阻塞");
}
System.out.println(count);
}
public static void main(String[] args) {
// 测试
ThreadSafeDemo1 tsd = new ThreadSafeDemo1();
tsd.init();
}
}
(2)案例2:电影院卖票
案例需求
模拟电影院卖票:开放3个窗口卖1000张电影票电影院卖票(案例场景可类似抢单、秒杀等场景)
实现步骤
1.创建一个SellTicket实现Runnable定义票总数和售票方法
2.构建多线程进行售票操作
参考案例
class SellTicket implements Runnable {
// 定义总票数
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets <= 0) {
System.out.println("票已售罄");
} else {
// 模拟卖票
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行卖票操作
tickets--;
System.out.println(Thread.currentThread().getName() + "已出一票,目前剩余" + tickets + "张票");
}
}
}
}
public class ThreadSafeDemo2 {
public static void main(String[] args) {
// 创建SellTicket类的对象
SellTicket st = new SellTicket();
// 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
基于这种方式可能出现同票或者负数票的情况,主要问题在于对同一资源的抢占,而线程执行具有随机性,在售票的过程中丢失了CPU的执行权导致问题发生
其数据安全问题出现原因在于:多线程环境、具有共享数据、有多条语句操作共享数据
线程安全问题解决:借助Java提供的同步代码块方式将多条语句操作共享数据的代码给锁起来,让任意时刻只有一个线程进去该代码块
# 参考执行结果
窗口3已出一票,目前剩余4张票
窗口1已出一票,目前剩余4张票
窗口2已出一票,目前剩余4张票
窗口3已出一票,目前剩余1张票
窗口2已出一票,目前剩余1张票
窗口1已出一票,目前剩余1张票
窗口3已出一票,目前剩余-2张票
窗口1已出一票,目前剩余-2张票
票已售罄
窗口2已出一票,目前剩余-2张票
票已售罄
票已售罄
票已售罄
线程安全:
class SaftSellTicket implements Runnable {
// 定义总票数
private int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
// 同步代码块:加锁
synchronized (obj){
if (tickets <= 0) {
System.out.println("票已售罄");
} else {
// 模拟卖票
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行卖票操作
tickets--;
System.out.println(Thread.currentThread().getName() + "已出一票,目前剩余" + tickets + "张票");
}
}
}
}
}
public class ThreadSafeDemo2 {
public static void main(String[] args) {
// 创建SellTicket类的对象
// SellTicket st = new SellTicket(); // 线程不安全
SaftSellTicket st = new SaftSellTicket(); // 借助同步代码块方式构建,保证线程安全
// 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
上述可通过同步代码块的方式保证线程安全,为了进一步明确加锁和释放锁的内容,可使用JDK5提供的Lock进行锁的控制
1.借助ReentrantLock实例化Lock对象
private ReentrantLock lock = new ReentrantLock();
2.在方法执行前执行加锁操作
lock.lock();
3.在资源释放的finally语句中执行释放锁操作
lock.unlock();
4.wait and notify
wait:在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样
【1】sleep和Wait的区别?
sleep必须指定时间 ,wait可以不指定时间
sleep和wait都可以让线程进入到冻结阻塞状态,都释放执行权(相同点)
持有锁的线程执行,sleep是指不释放锁 ; 持有锁的线程执行,wait是释放锁
sleep时间到会自动苏醒执行;而wait必须通过notify或者notifyall进行唤醒
class Demo
{
boolean flag = true;
public synchronized void f1()
{
while(flag)
{
/**
* 当flag满足true条件,执行wait方法之后
* 当前线程进入等待状态,此时wait之后的语句不能够执行
* 即方法f1不会让当前线程一直占有cpu的执行权
*/
try {
wait();//让当前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag = true;
notifyAll();//唤醒所有的线程
System.out.println("线程AAAAAAAAAA执行");
}
public synchronized void f2()
{
/**
* 当flag满足true条件时不会执行wait语句,
* 代表f2执行,则通过改变条件flag = false,
* 并唤醒所有的线程之后打印相应的内容
* 当所有的线程均被唤醒之后,假设还是f2抢到cpu的执行权,但
* 由于当前flag的值为false,则相应地会执行wait语句从而使得
* f2放弃cpu的执行权
* 以此类推,对f1进行分析
*/
while(!flag)
{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag = false;
notifyAll();
System.out.println("线程BBBBBBBBBB执行");
}
}
public class ThreadWaitTest {
/**
* 要求:两个线程轮流执行
* 打印 A B A B
*/
//定义公共的对象
Demo d = new Demo();
public void init()
{
new Thread(new Runnable(){
@Override
public void run() {
while(true)
{
d.f1();
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
while(true)
{
d.f2();
}
}
}).start();
}
public static void main(String[] args) {
new ThreadWaitTest().init();
}
/**
* 执行结果:
* 线程BBBBBBBBBB执行
* 线程AAAAAAAAAA执行
* 线程BBBBBBBBBB执行
* 线程AAAAAAAAAA执行
*/
}
📌案例分析
需求说明:三个线程轮流执行 A B C A B C A B C
class Print
{
int flag = 1;
public synchronized void f1()
{
/**
* 如果当前flag不为1,则锁定当前线程,但此时flag为1
* 则说明是f1抢占到cpu,执行之后的语句,另flag=2
* 随后唤醒所有线程并输出相应语句
* 当flag=2的时候,如果相应的f1或者是f3抢占到cpu,则
* 其相应的会执行wait语句而放弃当前cpu的执行权,只有当f2
* 抢占到cpu时能够继续执行相应操作,输出语句,随后指定下一个执行的内容
* 以此类推进行分析即可!
*/
while(!(flag==1))
{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag = 2;
notifyAll();
System.out.println("线程AAAAAAAAAA执行");
}
public synchronized void f2()
{
while(!(flag==2))
{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag = 3;
notifyAll();
System.out.println("线程BBBBBBBBBB执行");
}
public synchronized void f3()
{
while(!(flag==3))
{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag = 1;
notifyAll();
System.out.println("线程CCCCCCCCCC执行");
}
}
public class ThreadWaitTest2 {
Print p = new Print();
public void init()
{
new Thread(new Runnable(){
@Override
public void run() {
while(true)
{
p.f1();
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
while(true)
{
p.f2();
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
while(true)
{
p.f3();
}
}
}).start();
}
public static void main(String[] args) {
new ThreadWaitTest2().init();
}
/**
* 执行结果:
* 线程AAAAAAAAAA执行
* 线程BBBBBBBBBB执行
* 线程CCCCCCCCCC执行
* 线程AAAAAAAAAA执行
* 线程BBBBBBBBBB执行
* 线程CCCCCCCCCC执行
*/
}
【2】生产者和消费者问题
生产者和消费者模式概述
生产者消费者模式是一个经典的多线程协作的模式,实际上主要是包含了两类线程:生产者线程(用于生产数据)、消费者线程(用于消费数据),通过采用共享的数据区域对生产者和消费者的关系进行解耦,生产者/消费者只需对共享数据进行操作(生产/消费)而无需关心对方的行为。
Object类的等待和唤醒方法
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
/**
* 构建生产者和消费者进行交互的产品类
*/
class Product {
private int id;
private String name;
public Product() { }
public Product(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
/**
* 构建Table模拟容器操作
*/
class Table {
// 1.用LinkedList模拟桌子
LinkedList<Product> table = new LinkedList<>();
// 2.设定桌子的最大容量
public int max = 10;
// 3.获取当前桌子的产品数目
public int size() {
return table.size();
}
// 4.定义相应的事件:生产者生产产品、消费者消费产品
/**
* 生产者生产产品需要注意的问题:
* 当同时有多个厨师发现桌子上的食物缺少,同时想要进行生产,但此时并不需要太多的资源,
* 则此时他们是属于竞争关系的,因此需要对生产产品的方法加“锁”,使得每次只能有一个厨师放入产品
*/
public synchronized void put(Product p) {
/**
* 当当前放入的食品数目等于餐桌的最大容量,则厨师需要进入等待状态
*/
while (size() == max) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 如果当前放入产品的数目没有达到餐桌的最大容量,则生产者可以放入产品,随后唤醒所有消费者和生产者
* 假如说生产者已经满了,需要单独唤醒指定的内容,则可通过juc提供的相关内容实现
*/
table.add(p);
notifyAll();
}
public synchronized Product take() {
/**
* 消费者在取走产品的时候也要判断餐桌上是否有相应的产品,如果桌子上没有产品,则消费者需要进入等待
*/
while (size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果桌子上有产品,则取走最后一个产品随后唤醒所有的线程
Product p = table.removeLast();
notifyAll();
// 返回当前被取走的产品
return p;
}
}
/**
* 模拟生产者
*/
class Producer extends Thread{
private Table table ;
public Producer(Table table) {
super();
this.table = table;
}
@Override
public void run() {
int i = 0;
while(true)
{
//生产者生产一个产品
Product p = new Product(++i,"产品"+i);
//将生产的产品放入桌子上
table.put(p);
System.out.println("生产者生产了一个产品放到桌子上,产品id为"+p.getId());
//模拟生产的过程,休眠2s
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 模拟消费者
*/
class Customer extends Thread {
private Table table;
public Customer(Table table) {
super();
this.table = table;
};
@Override
public void run() {
while(true)
{
Product p = table.take();
System.out.println("消费者消费了一个产品,产品id为"+p.getId()+",产品名称为:"+p.getName());
//模拟消费者消费过程,休眠2s
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 模拟生产者消费者操作
*/
public class PCDemo {
/**
* 首先,要保证生产者和消费者操作的是同一张桌子
*/
final Table table = new Table();
public void init()
{
// 模拟5个生产者
for(int i=0;i<5;i++)
{
Producer p = new Producer(table);
p.start();
}
// 模拟3个消费者
for(int i=0;i<3;i++)
{
Customer c = new Customer(table);
c.start();
}
//单独新建一个线程,用于监测桌子上产品容量
new Thread(new Runnable(){
@Override
public void run() {
while(true)
{
System.out.println("当前桌子容量为:"+table.size());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
public static void main(String[] args) {
new PCDemo().init();
}
}
5.死锁
【1】死锁基础概念
死锁的基本概念
死锁是指两个或者两个以上的线程在执行的过程中,因为相互抢夺资源而造成的一种相互等待的状态,如果没有外力作用,他们是无法进行推进下去,此时系统处于死锁状态 ,系统产生了死锁,这些用于在互相等待的进程称为死锁进程
为什么会产生死锁?
- 因为系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
- 锁的使用不合理
如何避免死锁?
只要破坏产生死锁的任意一条条件即可破解死锁
产生死锁的四个条件
互斥条件:所谓的互斥条件是进程在某一时间内独占资源
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:进程在获得资源未使用完之前不能强行剥夺
循环等待条件:若干进程之间形成一种头尾连接而相互等待资源的关系
死锁案例
public class DeathLockDemo {
public static void main(String[] args) {
Object objA = new Object();
Object objB = new Object();
new Thread(()->{
while(true){
synchronized (objA){
//线程一
synchronized (objB){
System.out.println("BBBBBB");
}
}
}
}).start();
new Thread(()->{
while(true){
synchronized (objB){
//线程二
synchronized (objA){
System.out.println("AAAAAA");
}
}
}
}).start();
}
}
【2】如何避免死锁
破坏四个条件中的一个即可
public class DeathLock implements Runnable {
private String tag;
// 定义两个Object对象
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public void setTag(String tag) {
this.tag = tag;
}
public static void main(String[] args) {
DeathLock d1 = new DeathLock();
d1.setTag("a");
DeathLock d2 = new DeathLock();
d2.setTag("b");
Thread t1 = new Thread(d1, "t1");
Thread t2 = new Thread(d2, "t2");
t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
@Override
public void run() {
if (tag.equals("a")) {
synchronized (lock1) {
try {
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入lock1执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
try {
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入lock2执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
if (tag.equals("b")) {
synchronized (lock2) {
try {
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入lock2执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
try {
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入lock1执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
6.volatile关键字
【1】volatile基本概念
在java中每一个线程都有一块自己的工作内存区,其中每个线程在执行的时候是把所有线程共享的内存进行了一份值的拷贝,一个线程在工作的时候会锁定自己的内存工作区,把这些共享变量从主内存中拷贝到自己的工作内存中,当线程执行结束才会解锁,把自己的工作区的数据同步到主内存区域中
多线程场景中常见的一个问题就是当一个线程改变了变量另外一个线程能否立马感知,而Volatile关键字的主要作用是在多个线程之间共享数据,多个线程之间可见
Volatiled的作用就是强制线程到主内存(共享内存)中去读取变量,而不是在工作内存区中读取数据,从而实现了多个线程的变量之间的可见性。也就满足线程的安全的可见性
public class ThreadVolatileDemo extends Thread {
public volatile boolean isRunning = true;
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("进入run方法....");
int a = 0;
while (isRunning) {
// ...
}
System.out.println("线程停止....");
}
public static void main(String[] args) throws Exception {
ThreadVolatileDemo tv = new ThreadVolatileDemo();
tv.start();
Thread.sleep(1000);
tv.setRunning(false);
System.out.println("isRuning 的值已经被改变...false");
Thread.sleep(1000);
System.out.println(tv.isRunning);
}
}
// 分析:根据程序分析,如果isRunning没有指定为volatile,线程没有感知到isRunning属性的变化,从而导致在while条件判断时线程不断循环执行业务操作而无法进入到”打印线程停止这条语句”
【2】volatile关键字是非原子性的
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。
volatile关键字拥有多个线程之间的可见性,但是不具备同步性(就是原子性)
public class VolatileNoAtomicDemo {
public static void main(String[] args) {
// 1.volatile非原子操作:不同对象操作公共的static变量
VolatileNoAtomic[] arr =new VolatileNoAtomic[10];
for(int i=0;i<arr.length;i++) {
arr[i]=new VolatileNoAtomic();
}
for(int i=0;i<10;i++) {
arr[i].start();
}
// 2.volatile+synchronized:操作同一个对象的成员变量
VolatileAtomic va = new VolatileAtomic();
for(int i=0;i<10;i++) {
new Thread(va).start();
}
// 3.借助AtomicInteger实现
AtomicOper[] arr3 =new AtomicOper[10];
for(int i=0;i<arr3.length;i++) {
arr3[i]=new AtomicOper();
}
for(int i=0;i<10;i++) {
arr3[i].start();
}
}
}
// demo1:volatile无法保证原子性
class VolatileNoAtomic extends Thread{
private static volatile int count ;
public static void addCount() {
for(int i=0;i<10000;i++) {
count++;
}
System.out.println(count);
}
@Override
public void run() {
addCount();
}
}
// demo2:借助synchronized加锁保证原子性
class VolatileAtomic implements Runnable {
private volatile int count ;
// 定义对象锁
private Object lock = new Object();
public void addCount() {
for(int i=0;i<10000;i++) {
/**
* 多线程场景下volatile关键字修饰的变量情况
* 1.从共享数据中读取数据到本线程栈中
* 2.修改本线程栈中变量副本的值
* 3.将本线程栈中变量副本的值赋值给共享数据
*/
synchronized (lock){
count++;
}
}
System.out.println(count);
}
@Override
public void run() {
addCount();
}
}
// demo3:借助AtomicInteger实现原子操作
class AtomicOper extends Thread{
private static AtomicInteger count =new AtomicInteger(0);
public static void addCount() {
for(int i=0;i<10000;i++) {
count.incrementAndGet();
}
System.out.println(count);
}
@Override
public void run() {
addCount();
}
}
分析:如果要实现多个线程操作累加总和为100000,volatile+count++的组合并不能实现,因为count++并不是一个原子性操作,在执行的过程中可能会被其他线程打断,而volatile关键字并不能保证原子性,从而导致最终总和达不到100000
如果要解决上述问题,则可通过给count++加锁的方式将其打包为原子操作并且在操作的过程将本线程栈的值同步到共享数据(通过volatile关键字设定),还可借助原子操作类AtomicInteger实现递增
【3】乐观锁和悲观锁
synchronized VS CAS
- 相同点:
在多线程场景下,这两种方式均可确保共享数据的安全性
- 不同点:
synchronized(悲观锁):从最坏的角度出发,认为每次获取数据的时候,都有可能被修改。因此在每次操作共享数据之前都会上锁
CAS(乐观锁):从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。而是在修改共享数据的时候,会检查一下别人有没有修改过这个数据,如果数据被修改则再次获取现在最新的值,如果数据没有被修改则直接修改共享数据的值
7.java.util.concurrent.locks
java对应java.util.concurrent.locks包涉及几个锁的概念
锁 | 说明 |
---|---|
Lock锁 | 实现了比使用synchronized方法和语句可获得更广泛的锁定 |
Condition | 条件锁 |
ReadWriteLock | 读写锁 |
【1】Lock锁
Lock锁接口有一个实现类ReentrantLock 一个可重入的互斥锁lock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) {
LockTest lt =new LockTest();
lt.init();
}
LockPrint lp = new LockPrint();
public void init() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
lp.printString("AAAAAAAAAAAAAAAAAAAAAAAA");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
lp.printString("BBBBBBBBBBBBBBBBBBBBBBBBBBB");
}
}
}).start();
}
}
class LockPrint {
// 得到Lock锁
Lock lock = new ReentrantLock();
public void printString(String str) {
try {
lock.lock(); // 在当前位置加锁
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); // 在当前位置解锁
}
}
}
使用Lock锁与synchronized对比有什么区别?
等待可中断
当持有锁的线程如果长时间不释放锁,正在等待的线程可以放弃等待,处理其他事情
公平锁
多个线程在等待一个锁的时候,必须 按照申请锁的时间顺序获得锁, synchronized锁是非公平锁,Lock如果在创建的时候指定true则代表是公平锁.
绑定条件
条件锁
【2】Condition锁
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionTest {
public static void main(String[] args) {
ConditionTest ct =new ConditionTest();
ct.init();
}
ConditionPrint cp =new ConditionPrint();
public void init() {
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
cp.f1();
}
}
},"线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
cp.f2();
}
}
},"线程2").start();
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
cp.f3();
}
}
},"线程3").start();
}
}
class ConditionPrint {
Lock lock = new ReentrantLock();
// 通过lock锁 创建三个条件锁
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
// A B C顺序执行
int token = 1;
public void f1() {
lock.lock();
try {
while (token != 1) {
c1.await(); // 让线程c1进入等待
}
System.out.println(Thread.currentThread().getName());
token = 2;
// 单独唤醒线程2
c2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void f2() {
lock.lock();
try {
while (token != 2) {
c2.await(); // 让线程c1进入等待
}
System.out.println(Thread.currentThread().getName());
token = 3;
// 单独唤醒线程2
c3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void f3() {
lock.lock();
try {
while (token != 3) {
c3.await(); // 让线程c1进入等待
}
System.out.println(Thread.currentThread().getName());
token = 1;
// 单独唤醒线程2
c1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
【3】读写锁
读写锁是分为读锁和写锁,多个写锁之间是互斥的,多个读锁之间是不互斥的,读写锁之间也是互斥的. 读写锁能够提高并发读取的性能.
ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。
所有 ReadWriteLock 实现都必须保证 writeLock 操作的内存同步效果也要保持与相关 readLock 的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
public static void main(String[] args) {
ReadWriteLockTest rt =new ReadWriteLockTest();
rt.init();
}
ReadWriteLockPrint rwlp =new ReadWriteLockPrint();
Random r =new Random();
public void init() {
//开启三个线程 写入数据
for(int i=0;i<3;i++) {
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
rwlp.add(r.nextInt(100));
}
}
},"写线程"+i).start();
}
//开启五个线程进行读数据
for(int i=0;i<5;i++) {
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
rwlp.get(r.nextInt(100));
}
}
},"读线程"+i).start();
}
}
}
class ReadWriteLockPrint {
ReadWriteLock rwl = new ReentrantReadWriteLock(true);// 公平锁
List<Integer> list = new ArrayList<>();
public ReadWriteLockPrint() {
for (int i = 0; i < 100; i++) {
list.add(i);
}
}
public void add(Integer i) {
// 写数据 得到写锁
rwl.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "准备写入数据....");
Thread.sleep(1000);
list.add(i);
System.out.println(Thread.currentThread().getName() + "写入数据完毕....");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放写锁
rwl.writeLock().unlock();
}
}
public Integer get(Integer index) {
Integer i =null;
//得到读锁
rwl.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "准备读取数据....");
Thread.sleep(1000);
i =list.get(index);
System.out.println(Thread.currentThread().getName() + "读取数据完毕....");
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放读锁
rwl.readLock().unlock();
}
return i;
}
}
8.同步容器和并发容器
【1】同步容器
同步容器都是线程安全的,但是在某些场景下,需要再次加锁来保护复合操作的安全性
复合操作比如迭代(反复访问元素,遍历完集合中所有的元素进行其他操作),跳转(根据查找到的元素调到到指定的元素) ,这些复合操作在多线程中并发的修改容器时,可能会表现出其他错误行为(最经典的案例 ConcurrentModificationException
)
同步容器比如Vector、Hashtable等,其实这些同步容器仅仅是在底层代码通过synchronized 底层进行加锁。 使得每次只允许一个线程进入程序中, 在现在高并发时代,要求在保证安全的同时必须拥有足够好的性能 。 --->并发容器。
import java.util.Vector;
public class VectorTest {
public static void main(String[] args) {
VectorTest vt = new VectorTest();
vt.init();
}
VectorOpr vo = new VectorOpr();
public void init() {
new Thread(new Runnable() {
@Override
public void run() {
vo.printVector();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
vo.deleteVector();
}
}).start();
}
class VectorOpr {
Vector<Integer> list = new Vector<>();
public VectorOpr() {
for (int i = 0; i < 10; i++) {
list.add(i);
}
}
public synchronized void printVector() {
for (Integer i : list) {
System.out.println(i);
}
}
public synchronized void deleteVector() {
for (int i = list.size() - 1; i >= 0; i--) {
list.remove(i);
}
}
}
}
【2】并发容器
容器 | 说明 |
---|---|
ConcurrentHashMap | 是替代Map同步的并发容器的实现 |
CopyOnWriteArrayList | 是List的并发容器的实现 |
ConcurrentLinkedQueue | 是Queue的并发容器的实现 |
CopyOnWriteArraySet | 是Set的并发容器的实现 |
ConcurrentHashMap
支持获取的完全并发和更新的所期望可调整并发的哈希表。此类遵守与 Hashtable 相同的功能规范,并且包括对应于 Hashtable 的每个方法的方法版本。不过,尽管所有操作都是线程安全的,但获取操作不 必锁定,并且不 支持以某种防止所有访问的方式锁定整个表。此类可以通过程序完全与 Hashtable 进行互操作,这取决于其线程安全,而与其同步细节无关。
ConcurrentHashMap和Map用法基本一致,不同点是ConcurrentHashMap可以在并发环境下正确执行
HashMap源码底层解构,HashMaps 维护了一个数组, 而其中每个数组的元素又是一个链表
(1)HashMap底层源码的解读
HashMap底层实现原理分析
HashMap的底层实现原理:HashMap中的数据结构是数组+单链表的组合,以键值对(key-value)的形式存储元素的,通过put()和get()方法储存和获取对象
a.通过hashcode找到数组中某一元素
b.通过key的equals方法在链表中找到key对应的value值
源码解读
常用参数
/** 初始容量,默认16 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** 最大初始容量,2^30 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/** 负载因子,默认0.75,负载因子越小,hash冲突机率越低 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** 初始化一个Entry的空数组 */
static final Entry<?,?>[] EMPTY_TABLE = {};
/**将初始化好的空数组赋值给table,table数组是HashMap实际存储数据的地方,并不在EMPTY_TABLE数组中 */
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/** HashMap实际存储的元素个数 */
transient int size;
/** 临界值(HashMap 实际能存储的大小),公式为(threshold = capacity * loadFactor) */
int threshold;
/** 负载因子 */
final float loadFactor;
/** HashMap的结构被修改的次数,用于迭代器 */
transient int modCount;
链表解构
// Node是单向链表,它实现了Map.Entry接口 它是根据hash计算存储的位置
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
构造函数
// 构造函数1
public HashMap(int initialCapacity, float loadFactor) {
// 指定的初始容量非负
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 如果指定的初始容量大于最大容量,置为最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 填充比为正
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);// 新的扩容临界值
}
// 构造函数2
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 构造函数3
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 构造函数4用m的元素初始化散列映射
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
HashMap的存取值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 参数分析:
参数1:hash(key) 根据key值计算出一个hashcode值 用于存储数据的位置
参数2:是要存储的key
参数3:是要存储的value值
参数4:
添加键值对put(key,value)的过程:
a.判断键值对数组tab[]是否为空或为null,否则以默认大小resize();
c.根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则转入3
c.判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get(key)执行过程分析:
get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可
(2)ConcurrentHashMap底层源码的解读
ConcurrentHashMap采用了分段锁的设计,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。相比于对整个Map加锁的设计,分段锁大大的提高了高并发环境下的处理能力。但同时,由于不是对整个Map加锁,导致一些需要扫描整个Map的方法(如size(), containsValue())需要使用特殊的实现,另外一些方法(如clear())甚至放弃了对一致性的要求
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。ConcurrentHashMap中的HashEntry相对于HashMap中的Entry有一定的差异性:HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性,
也就是说ConcurrentHashMap采用分段锁设计,把底层拆分为最多支持16个并发 可以提高效率。
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapTest {
public static void main(String[] args) {
// 在并发环境下 使用ConcurrentHashMap 代替HashMap即可其他用法完全一致
ConcurrentHashMap cuHashMap =new ConcurrentHashMap();
cuHashMap.put("username", "guigu");
cuHashMap.put("age", "13");
cuHashMap.put("sex", "femal");
cuHashMap.forEach((key,value)->System.out.println(key+"--"+value));
}
}
CopyOnWrite
Copy-on-Write 简称COW 是一种程序中设计的优化策略
JDK常见的COW容器分为CopyOnWriteArrayList 和CopyOnWriteArraySet
什么是CopyOnWrite容器
CopyOnWrite容器是 写时复制的容器, 通俗的将就是当我们往一个容器中添加元素时,不是直接把元素添加到容器,而是先把容器复制一份,然后把元素添加到新的容器中,添加完元素后,再把指针指向新的容器, 这样的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁, 读和写是不同的容器 ,就是最简单的读写分离的思想
(1)CopyonWriteArrayList
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListTest {
public static void main(String[] args) {
new CopyOnWriteArrayListTest().init();;
}
DoSth ds = new DoSth();
public void init() {
new Thread(new Runnable() {
@Override
public void run() {
ds.work("1");
}
}, "线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
ds.work("1");
}
}, "线程2").start();
new Thread(new Runnable() {
@Override
public void run() {
ds.work("2");
}
}, "线程3").start();
}
class DoSth {
// Collection<String> list = new ArrayList<>();
Collection<String> list =new CopyOnWriteArrayList<>(); //并发容器
public void work(String value) {
// 如果容器中没有指定的数据 则添加到容器中 如果存在则迭代数据
if (!list.contains(value)) {
list.add(value);
} else {
for (String str : list) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str);
}
}
}
}
}
(2)CopyOnWriteArraySet
Collection<String> list = new ArrayList<>();
CopyOnWriteArrayList<String> list =new CopyOnWriteArrayList<>(); //并发容器 list集合的替代
Collection<Integer> col =new CopyOnWriteArraySet<>() ;// 并发容器set集合的替代
🔖并发的Queue阻塞队列ArrayBlockingQueue
ArrayBlockingQueue:基于数组的阻塞队列的实现,在ArrayBlockingQueue内部,维护一个定长数组,以便缓存队列中的数据对象,其内部是没有实现读写分离。也就是说生产者和消费者不能完全并行,长度需要定义,可以指定先进先出或者先进后出
LinkedBlockingQueue:基于链表的阻塞队列, 同ArrayBlockingQueue类似,其内部也维护一个缓冲队列,这个队列是由一个链表组成,LinkedBlockingQueue效率十分高,原因是内部采用分离锁(读写分离成两个锁),从而实现生产者和消费者并行。并且是一个无界队列
PriorityBlockingQueue:基于优先级的阻塞队列, 是根据传递的构造函数决定优先级,就是必须需要实现一个Compator对象来决定
DelayQueue:带有延迟时间的Queue,元素只有指定的时间到了才能从队列中获取该元素
SynchronousQueue:一种没有缓冲的队列,生产者的数据会直接被消费者消费
(1)ArrayBlockingQueue/ LinkedBlockingQueue
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class UseQueue {
public static void main(String[] args) throws Exception {
// ConcurrentLinkedQueue:高性能 无阻塞 无界队列
ConcurrentLinkedQueue<String> q1 = new ConcurrentLinkedQueue<>();
// 存储数据
q1.offer("a");
q1.offer("b");
q1.offer("c");
q1.offer("d");
q1.offer("e");
System.out.println(q1);
System.out.println(q1.poll());// 取出并从队列中移除元素
System.out.println(q1);
System.out.println(q1.size());
System.out.println(q1.peek());// 取出元素 不移除
System.out.println(q1);
// ArrayBlockingQueue:底层是数组,阻塞队列、有界队列
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(5);
arrayBlockingQueue.put("a");
arrayBlockingQueue.put("b");
arrayBlockingQueue.put("c");
arrayBlockingQueue.put("d");
arrayBlockingQueue.put("e");
System.out.println(arrayBlockingQueue);
arrayBlockingQueue.poll();
arrayBlockingQueue.put("f");
System.out.println(arrayBlockingQueue);
// arrayBlockingQueue.put("q");
// arrayBlockingQueue.put("w");
// 指定一定时间 如果没有加入成功则返回失败
System.out.println(arrayBlockingQueue.offer("aqe", 3, TimeUnit.SECONDS));
// LinkedBlockingQueue:底层是链表,阻塞队列 无界队列
LinkedBlockingQueue<String> linkedBlockingQueue =new LinkedBlockingQueue<>();
linkedBlockingQueue.offer("aa");
linkedBlockingQueue.offer("nn");
linkedBlockingQueue.offer("qq");
linkedBlockingQueue.offer("ww");
linkedBlockingQueue.offer("ee");
linkedBlockingQueue.offer("gg");
linkedBlockingQueue.offer("ff");
System.out.println(linkedBlockingQueue.size());
linkedBlockingQueue.forEach(System.out::println);
}
}
(2)PriorityBlockingQueue
public class Task implements Comparable<Task> {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int compareTo(Task task) {
return this.id > task.getId() ? 1 : (this.id < task.getId() ? -1 : 0);
}
@Override
public String toString() {
return "Task [id=" + id + ", name=" + name + "]";
}
}
public class PriorityBlockingQueueDemo {
public static void main(String[] args) throws Exception {
PriorityBlockingQueue<Task> q1 =new PriorityBlockingQueue<>();
Task t1 =new Task();
t1.setId(3);
t1.setName("id为3");
Task t2 =new Task();
t2.setId(1);
t2.setName("id为1");
Task t3 =new Task();
t3.setId(4);
t3.setName("id为4");
Task t4 =new Task();
t4.setId(2);
t4.setName("id为2");
q1.add(t1);
q1.add(t2);
q1.add(t3);
q1.add(t4);
System.out.println("容器q是:"+q1);
System.out.println(q1.take().getId());
System.out.println("容器q是:"+q1);
// PriorityBlockingQueue并不是完全按照优先级排序后执行,而是每次take会取出优先级最高的数据并排序一次
}
}
(3)DelayQueue
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class WangMin implements Delayed {
private String name;
private String id;
// 截止时间
private long endTime;
// 定义时间工具类
private TimeUnit timeUnit = TimeUnit.SECONDS;
public WangMin(String name, String id, long endTime) {
this.name = name;
this.id = id;
this.endTime = endTime;
}
// 相互比较进行排序
@Override
public int compareTo(Delayed delayed) {
WangMin w = (WangMin) delayed;
return this.getDelay(this.timeUnit) - w.getDelay(this.timeUnit) > 0 ? 1 : 0;
}
// 主要是判断是否到了截止时间
@Override
public long getDelay(TimeUnit unit) {
return endTime - System.currentTimeMillis();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
import java.util.concurrent.DelayQueue;
public class WangBa implements Runnable{
//定时执行的任务
private DelayQueue<WangMin> queue =new DelayQueue<>();
public boolean yingye =true;
public void shangji(String name ,String id, int money) {
WangMin man =new WangMin(name,id,money*1000+System.currentTimeMillis());
System.out.println("网名:"+man.getName()+"身份证号:"+man.getId()+"交钱:"+money+"块钱,开始上机...");
//把网民加入到队列进行监控
this.queue.add(man);
}
public void xiaji(WangMin man) {
System.out.println("网名:"+man.getName()+"身份证号:"+man.getId()+"时间到下机....");
}
@Override
public void run() {
while(yingye) {
try {
WangMin man =queue.take();
xiaji(man);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
System.out.println("网吧开始营业...");
WangBa guigu=new WangBa();
Thread shangwang =new Thread(guigu);
shangwang.start();
guigu.shangji("张三", "123", 1);
guigu.shangji("李四", "234", 5);
guigu.shangji("赵六", "423", 10);
}
}
9.线程池
【1】线程池的基本概念
为什么有线程池?
单个任务处理的时间很短而请求的数目却是巨大的。
构建服务器应用程序的一个简单的模型:每当一个请求到达就创建一个新线程,然后在新线程中为请求服务。实际上,对于原型开发这种方法工作得很好,但如果试图部署以这种方式运行的服务器应用程序,那么这种方法的严重不足就很明显。
每个请求对应一个线程(thread-per-request)方法的不足之一是:为每个请求创建一个新线程的开销很大;为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源更多。
除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。在一个 JVM 里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或“切换过度”。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目。
线程池处理过程分析
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但需要等到其他线程完成后才启动。
在初始创建的时候创建一定数量的线程,由这些线程组成线程池,重复管理线程,避免创建大量的线程增加开销,提高响应速度
【2】JDK提供的创建线程池的方式
newCachedThreadPool 返回一个可以根据实际情况调整线程个数的线程池。可以动态的变化,不限制最大线程数量,空闲的线程直接执行相关的任务,如果没有任务则不创建线程。
newSingleThreadExecutor
返回一个线程的线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class UseExecutorDemo1 {
public static void main(String[] args) {
//开启是十个任务 每个任务是循环十次
//创建一个线程池
ExecutorService pool = Executors.newSingleThreadExecutor();
//创建可调整的线程池
ExecutorService pool2 =Executors.newCachedThreadPool();
//创建指定线程的线程池
ExecutorService pool3 =Executors.newFixedThreadPool(10);
//开启十个任务
for(int i =0;i<10;i++) {
final int task=i;
//把这个十个任务放到线程池中
pool3.execute(new Runnable() { // 通过execute或者submit执行任务
@Override
public void run() {
//每个线程是循环十次
for(int k=0;k<10;k++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程的名称是:"+Thread.currentThread().getName()+"运行第"+task+"个任务");
}
}
});
}
//关闭线程池
pool3.shutdown();
}
}
【3】自定义线程池
🔖概念说明
如果Excutors工厂类提供的线程池无法满足业务需求,可通过ThreadPoolExecutor自定义完成线程池
参数 | 说明 |
---|---|
int corePoolSize | 核心线程的数量 |
int maximumPoolSize | 最大线程的数量 |
long keepAliveTime | 超出核心线程数量意外的线程空余存活时间 |
TimeUnit unit | 存活时间的单位 |
BlockingQueue<Runnable> workQueue | 保存待执行任务的队列 |
ThreadFactory threadFactory | 创建新线程使用的工厂 |
RejectedExecutionHandler handler | 当任务无法执行时的处理器 |
参考java.util.concurrent.ThreadPoolExecutor,关注线程池最重要的几个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
自定义线程池思路说明
1.判断线程池判断核心线程池里的线程是否都在执行任务
否:创建一个新的工作线程来执行任务
是:进入下个判断节点
2.判断线程池判断工作队列是否已满
否:将新提交的任务存储在这个工作队列里
是:进入下个判断节点
3.线程池判断线程池的线程是否都处于工作状态
否:创建一个新的工作线程来执行任务
是:交给饱和策略(拒绝策略)来处理这个任务
饱和策略概念
饱和策略 | 说明 |
---|---|
CallerRunsPolicy | 只要线程池没关闭,就直接用调用者所在线程来运行任务 |
AbortPolicy | 直接抛出 RejectedExecutionException 异常 |
DiscardPolicy | 悄悄把任务放生,不执行 |
DiscardOldestPolicy | 抛弃队列中等待最久的任务,然后再尝试调用 execute() |
自定义策略 | 实现RejectedExecutionHandler 接口自定义策略 |
🔖参考实现
自定义线程
public class MyTask implements Runnable {
private int taskId;
private String taskName;
public MyTask(int taskId, String taskName) {
super();
this.taskId = taskId;
this.taskName = taskName;
}
public int getTaskId() {
return taskId;
}
public void setTaskId(int taskId) {
this.taskId = taskId;
}
public String getTaskName() {
return taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
try {
System.out.println("run taskId ="+this.taskId);
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return Integer.toString(this.taskId);
}
}
自定义饱和策略(拒绝策略)
// 自定义饱和策略或者拒绝策略
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
public class MyReject implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("自定义饱和/拒绝策略处理...");
System.out.println("当前任务被拒绝执行...拒绝执行的任务id是:"+r.toString());
}
}
自定义线程池
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MyThreadPoolExecutorDemo {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
1,// corePoolSize,
2,// maximumPoolSize,
60,// keepAliveTime,
TimeUnit.SECONDS,// unit
new ArrayBlockingQueue<Runnable>(3),// workQueue,使用有界队列
new MyReject()// 拒绝策略(使用自定义策略)
);
MyTask mt1 = new MyTask(1, "任务1");
MyTask mt2 = new MyTask(2, "任务2");
MyTask mt3 = new MyTask(3, "任务3");
MyTask mt4 = new MyTask(4, "任务4");
MyTask mt5 = new MyTask(5, "任务5");
MyTask mt6 = new MyTask(6, "任务6");
pool.execute(mt1);
pool.execute(mt2);
pool.execute(mt3);
pool.execute(mt4);
pool.execute(mt5);
pool.execute(mt6);
pool.shutdown();
}
}
使用无界队列
public class UseExecutorDemo2 implements Runnable{
private static AtomicInteger count =new AtomicInteger(0);
@Override
public void run() {
try {
int temp =count.incrementAndGet();
System.out.println("任务:"+temp);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
LinkedBlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
ExecutorService service =new ThreadPoolExecutor(
5,
10,
120,
TimeUnit.SECONDS,
queue);
for(int i=0;i<20;i++) {
service.execute(new UseExecutorDemo2());
}
Thread.sleep(1000);
System.out.println("queue size:"+queue.size());
}
}
10.同步工具
【1】并发工具类(集合相关)
🔖Hashtable
Hashtable出现的原因
在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能存在问题)。为了保证数据的安全性可以使用Hashtable替换,但是Hashtable的效率低下
public class HashtableDemo {
public static void main(String[] args) throws InterruptedException {
Hashtable<String,String> hm = new Hashtable<>();
Thread t1 = new Thread(()->{
for (int i = 0 ;i<25;i++){
hm.put(""+i,"线程"+i);
}
});
Thread t2 = new Thread(()->{
for (int i = 25 ;i<50;i++){
hm.put(""+i,"线程"+i);
}
});
// 启动线程,并通过适当设定sleep确保数据可正常添加完成
t1.start();
t2.start();
Thread.sleep(1000);
// 循环打印Hashtable集合的内容
for (int i=0;i<50;i++){
System.out.println(hm.get(i+""));
}
}
}
🔖ConcurrentHashMap
ConcurrentHashMap出现的原因
HashMap:多线程环境下使用可能存在问题
Hashtable:为确保数据安全性引入Hashtable,但效率低下(线程安全,但会锁表导致效率低下)
ConcurrentHashMap:解决HashMap、Hashtable的不足之处(线程安全,JDK7、8中底层原理不同)
体系结构
public class ConcurrentHashMapDemo {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String,String> chm = new ConcurrentHashMap<>();
Thread t1 = new Thread(()->{
for (int i = 0 ;i<25;i++){
chm.put(""+i,"线程"+i);
}
});
Thread t2 = new Thread(()->{
for (int i = 25 ;i<50;i++){
chm.put(""+i,"线程"+i);
}
});
// 启动线程,并通过适当设定sleep确保数据可正常添加完成
t1.start();
t2.start();
Thread.sleep(1000);
// 循环打印Hashtable集合的内容
for (int i=0;i<50;i++){
System.out.println(chm.get(i+""));
}
}
}
【2】并发工具类(JUC)
JUC包中包含四个实现同步的工具类
工具类 | 说明 |
---|---|
Semaphore | 代表一个计数信号量 |
CountDownLatch | 用于保持给定数目的信号,事件或添加之前进行阻塞 |
CyclicBarrier | 是一个可重置的多路同步点 |
Exchanger | 允许两个线程在collection点交换对象 |
🔖Semaphore
基本概念
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。
使用场景
可以用于访问特定资源的线程数量
例如”通行证概念”
参考案例
public class SemaphoreDemo {
public static void main(String[] args) {
new SemaphoreDemo().init();
}
DoSth ds = new DoSth();
public void init() {
// 形参接收的是允许的许可数
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 10; i++) {
final int temp = i;
new Thread(new Runnable() {
@Override
public void run() {
// 在进行工作之前必须有许可
try {
// 获取许可
semaphore.acquire();
// 模拟业务操作
ds.work();
// 释放许可
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程" + i).start();
}
}
class DoSth {
public void work() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名称是:" + Thread.currentThread().getName());
}
}
}
使用线程池
public class SemaphorePoolDemo {
public static void main(String[] args) {
// 定义线程池
ExecutorService service = Executors.newCachedThreadPool();
// 定义信号量
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
// 获取许可
semaphore.acquire();
System.out.println("线程" + Thread.currentThread().getName() + ",进入,当前已经有" + (3 - semaphore.availablePermits()) + "个并发");
// 模拟业务操作
Thread.sleep((long) (Math.random() * 10000));
System.out.println("线程" + Thread.currentThread().getName() + ",即将离开....");
// 释放许可
semaphore.release();
System.out.println("线程" + Thread.currentThread().getName() + ",已离开,当前有" + (3 - semaphore.availablePermits()) + "个并发");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
service.execute(runnable);
}
}
}
🔖CountDownLatch
基本概念
一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
用给定的计数初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。
场景说明
使用场景:让某一条线程等待其他线程执行完毕之后再执行
方法 | 说明 |
---|---|
public CountDownLatch(int count) | 定义一个计数器,参数传递线程数,表示等待线程数量 |
public void await() | 让线程等待(当计数器为0会唤醒等待的线程) |
public void countDown() | 线程执行完毕时调用,计数器-1 |
案例参考
赶校车:多人赶校车,待所有人聚集后方发车
public class CountDownLatchDemo {
public static void main(String[] args) {
new CountDownLatchDemo().init();
}
CountDownLatch cdl = new CountDownLatch(3);
Random r = new Random();
public void init() {
// 模拟A线程
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("A同学正在赶来的路上....");
Thread.sleep(r.nextInt(5000));
System.out.println("A同学已经到达集合点....");
cdl.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A同学").start();
// 模拟B线程
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("B同学正在赶来的路上....");
Thread.sleep(r.nextInt(5000));
System.out.println("B同学已经到达集合点....");
cdl.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B同学").start();
// 模拟C线程
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("C同学正在赶来的路上....");
Thread.sleep(r.nextInt(5000));
System.out.println("C同学已经到达集合点....");
cdl.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C同学").start();
// 模拟父线程(在cdl计数到达0之前线程一直阻塞)
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("司机正等待所有同学到达集合点...");
cdl.await();// 在计数达到0之前当前线程一直阻塞
System.out.println("所有的同学都已经到达,出发...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "司机叔叔").start();
}
}
🔖CyclicBarrier
基本概念
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier
参考案例
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
ExecutorService service = Executors.newCachedThreadPool();
Random r = new Random();
// 三个人共同执行五个任务,只有当三个人同时到达,才能执行下一个任务
for (int i = 0; i < 3; i++) {
service.execute(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
try {
Thread.sleep(r.nextInt(5000));
System.out.println(Thread.currentThread().getName() + "已到达" + (j + 1)
+ ",现在共有" + (cyclicBarrier.getNumberWaiting() + 1) + "个线程已到达");
if (cyclicBarrier.getNumberWaiting() + 1 == 3) {
System.out.println("所有线程都已经到达集合点,进行下一步操作...");
} else {
System.out.println("等待其他线程到达");
}
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
});
}
service.shutdown();
}
}
CyclicBarrier和CountDownLatch的区别
CountDownLatch 允许一个或多个线程等待一些特定的操作完成,而这些操作是在其它的线程中进行的,也就是说会出现 等待的线程 和被等的线程这样分明的角色;
CountDownLatch 构造函数中有一个 count 参数,表示有多少个线程需要被等待,对这个变量的修改是在其它线程中调用 countDown 方法,每一个不同的线程调用一次 countDown 方法就表示有一个被等待的线程到达,count 变为 0 时,latch(门闩)就会被打开,处于等待状态的那些线程接着可以执行;
CountDownLatch 是一次性使用的,也就是说latch门闩只能只用一次,一旦latch门闩被打开就不能再次关闭,将会一直保持打开状态,因此 CountDownLatch 类也没有为 count 变量提供 set 的方法;