Java并发编程学习笔记1
目录
公号:码农充电站pro
1,查看线程的方法
Window 系统
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist
查看进程taskkill
杀死进程
Linux 系统
ps -fT -p <PID>
查看某个进程(PID)的所有线程top
按大写 H 切换是否显示线程top -H -p <PID>
查看某个进程(PID)的所有线程
Java 命令
jps
查看所有 Java 进程jstack <PID>
查看某个 Java 进程(PID)的所有线程状态jconsole
来查看某个 Java 进程中线程的运行情况(图形界面)
要使用 jconsole 命令,需要以如下方式运行你的 java 类:
java
-Djava.rmi.server.hostname=`ip地址`
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=`连接端口`
-Dcom.sun.management.jmxremote.ssl=是否安全连接
-Dcom.sun.management.jmxremote.authenticate=是否认证
java类
2,Java 线程的创建方式
1,继承 Thread 类
Thread
类实现了 Runnable
接口,我们可以通过继承 Thread
类并重写 run
方法来创建线程。
// 创建线程的第一种方式:
// 继承 Thread 类,重写 run 方法,调用 start
public class TestThread extends Thread {
// 用来停止线程
private boolean isStop = true;
@Override
public void run() {
if (!isStop) {
System.out.println("我是一个线程。。。" + Thread.currentThread().getName());
}
}
// 正常停止线程的方式,建议使用本方式
public void stopThread() {
isStop = true;
}
public static void main(String[] args) {
TestThread t = new TestThread();
t.start(); // 调用 start 方法
new TestThread().start();
// 使用 Lamda(Java 8 提供的语法) 表达式
new Thread(()-> {
System.out.println("我就是这个线程。。。" + Thread.currentThread().getName());
}).start();
}
}
线程中重要的方法:
static void sleep
:线程休眠- 每个对象都有一把锁,sleep 只释放 cpu 资源,而不会释放锁,因此锁依然是被当前线程占有(wait 方会释放锁)
- 调用 sleep 会让当前线程从
Running
进入Timed Waiting
状态(阻塞) - 其它线程可以使用
t.interrupt
方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException
t
是正在 sleep 的线程对象
- 用
TimeUnit
的 sleep 代替 Thread 的 sleep 来获得更好的可读性
static void yield
:线程礼让- 将当前线程的的 CPU 控制权让出来,让 CPU 重新选择要执行的线程(有可能礼让之后,CPU 还是选择了当前线程)
- 调用 yield 会让当前线程从
Running
进入Runnable
就绪状态,然后调度执行其它线程
static Thread currentThread
:获取当前线程void start
:start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。- 每个线程对象的start方法只能调用一次,如果调用了多次会出现
IllegalThreadStateException
异常 - start 方法调用之前,线程的状态是
NEW
- start 方法调用之后,线程的状态是
RUNNABLE
- 每个线程对象的start方法只能调用一次,如果调用了多次会出现
void run
:线程启动后要调用的方法void setPriority
: 更改线程优先级,应该在 start 之前调用- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
MIN_PRIORITY = 1
NORM_PRIORITY = 5
MAX_PRIORITY = 10
int getPriority
:获取线程优先级boolean isAlive
:测试线程是否存活void join
:等待线程终止- 让当前线程执行完之后,再去执行其它线程(其它线程处于阻塞状态)
join
函数的底层原理是wait
(保护性暂停模式)
join(long n)
:等待线程运行结束,最多等待 n 毫秒getState
:获取线程当前状态,Thread.State
类中定义了 6 个线程状态- 线程停止之后就不能再次 start 了,一个线程只能 start 一次
setDaemon
:设置守护线程,一般在 start 方法之前设置- 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。
- 有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
getId
:获取线程 IDgetName
:获取线程名setName
:设置线程名interrupt
:打断线程- 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出
InterruptedException
,并清除打断标记 - 如果打断的正在运行的线程,则会设置打断标记 ,但并不会影响被打断线程的运行,被打断线程可以根据打断标记(
isInterrupted
),去进行下一步处理(比如停止运行) - park 的线程被打断,也会设置打断标记
- 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出
isInterrupted
:判断线程是否被打断,不会清除打断标记static interrupted
:判断当前线程是否被打断,会清除打断标记isAlive
:判断线程是否存活(没有运行完毕)
关于线程停止:不推荐使用 stop/destroy
方法(已经废弃),建议使用一个标志位来终止线程。
不推荐使用的方法:
stop
:停止线程运行,是暴力停止,不推荐使用suspend
:挂起(暂停)线程运行,会破坏同步代码块,不推荐使用- 替代方法是
wait
- 替代方法是
resume
:恢复线程运行,会破坏同步代码块,不推荐使用- 替代方法是
notify
- 替代方法是
join
方法的一个例子:
public class TestRunnableJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程 vip 来了:" + i);
try {
Thread.sleep(1000); // 要注意 sleep 只释放 cpu 资源,但是不释放锁,所以锁依然是被当前线程占有
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 启动线程
TestRunnableJoin j = new TestRunnableJoin();
Thread t = new Thread(j);
t.start();
// 主线程
for (int i = 0; i < 100; i++) {
if (i == 50) {
t.join(); // 让 t 线程执行完,再往下走,所有别的线程均阻塞
// 注意:join 的愿意是让当前线程(这里就是主线程)陷入“等待”状态,
// 等 join 的这个线程(这里就是 t)执行完成后,再继续执行当前线程(主线程)
}
System.out.println("Main Thread:" + i);
}
}
}
2,实现 Runnable 接口
从继承的角度来说,使用 Runnable 接口更好,因为还可以继承其它类。如果使用的是继承 Thread 的方式,就不能再继承其它类了,因为 Java 不支持多继承。
Java 中的 Runnable 接口:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
实现 Runnable 接口创建线程:
// 创建线程的第二种方式:
// 实现 Runnable 接口,重写 run 方法,调用 start
public class TestRunnable implements Runnable {
@Override
public void run() {
System.out.println("我是线程。。。" + Thread.currentThread().getName());
}
public static void main(String[] args) {
TestRunnable r = new TestRunnable();
Thread t = new Thread(r, "我是第一个线程");
t.start();
new Thread(r, "我是第二个线程").start();
}
}
3,实现 Callable 接口
继承 Thread 与实现 Runnable接口的方式都无法获取线程的执行结果。
创建线程的第三种方式:实现 Callable 接口,该方式可以获取线程的执行结果。
- 要重写 call 方法,call 方法有返回值,返回值的类型是 Callable<> 的泛型
- 一般与 ExecutorService 配合来使用
Future
是一个接口,是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。
Future 接口中声明了5个方法:
isCancelled
方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 trueisDone
方法表示任务是否已经完成,若任务完成,则返回trueget()
方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回get(long timeout, TimeUnit unit)
用来获取执行结果- 如果在指定时间内,还没获取到结果,就直接返回 null
cancel
方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false- 参数
mayInterruptIfRunning
表示是否允许取消正在执行却没有执行完毕的任务 - 如果设置true,则表示可以取消正在执行过程中的任务。
- 如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false
- 如果任务正在执行,若 mayInterruptIfRunning设置为true,则返回true;若mayInterruptIfRunning设置为false,则返回false
- 如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true
- 参数
FutureTask
是 Future
接口的一个唯一实现类。
示例:
// call 方法的返回值的类型是 Callable<> 的泛型
public class TestCallable implements Callable<Boolean> {
@Override
public Boolean call() {
System.out.println("我是线程。。。" + Thread.currentThread().getName());
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable c1 = new TestCallable();
TestCallable c2 = new TestCallable();
TestCallable c3 = new TestCallable();
// 创建执行服务 线程池
ExecutorService service = Executors.newFixedThreadPool(3);
// 提交执行 r1 r2 r3 是 call 方法的返回值
Future<Boolean> r1 = service.submit(c1);
Future<Boolean> r2 = service.submit(c2);
Future<Boolean> r3 = service.submit(c3);
// 获取返回结果 call 方法的返回值
boolean rs1 = r1.get(); // get 方法会阻塞直到任务返回结果
boolean rs2 = r2.get();
boolean rs3 = r3.get();
// 关闭服务
service.shutdownNow();
}
}
3,Java 线程池
详见:Java 线程池 ThreadPoolExecutor
一个示例:
// 线程池
public class TestThreadPool {
public static void main(String[] args) {
// 1 创建服务,创建了一个拥有 10 个线程的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// 2 执行线程
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
// 3 关闭线程
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
4,Java 线程同步 synchronized
在 Java 中,线程同步使用 synchronized
关键字(也称对象锁),synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
其有两种使用方式:
synchronized
修饰方法- 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {}
- 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {}
- 关键字在实例方法上,锁为当前实例
synchronized
修饰代码块:synchronized(obj) {}
- 其实 obj 就是锁对象
- 注意:obj 应该是变化的对象,即是需要增删改的对象,否则会不起作用
- obj 的选择是关键
synchronized 的原理:
- Java多线程的锁都是基于对象的,Java中的每一个对象都可以作为一个锁
- 每个 Java 对象都有一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程就会阻塞,synchronized 方法一旦执行,就会独占该锁,其它想获取该锁的线程就会阻塞
- Java 中的类锁:类锁其实也是对象锁
- Java类只有一个Class对象(可以有多个实例对象,多个实例共享这个Class对象),而 Class对象也是特殊的Java对象
- synchronized 锁的释放,有两种情况:
- 获取锁的线程执行完了该代码块,然后线程释放对锁的占有
- 或者线程执行发生异常,此时JVM会让线程自动释放锁
synchronized 的底层原理: 每个 Java 对象实际上对应了一个 Monitor 锁(由操作系统提供),同一个 Java 对象对应了相同的 Monitor 锁,不同的 Java 对象对应了不同的 Monitor 锁。
- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 中的 Owner 置为 Thread-2
- 在 Thread-2 持有锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList 阻塞队列
- Thread-2 执行完同步代码块后,会唤醒 EntryList 中的线程,被唤醒的线程会再去竞争 Monitor 锁
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足,进入 WAITING 状态的线程
注意:
- synchronized(obj) 必须是进入同一个对象的 monitor 才有上述效果
- 不加 synchronized(obj) 的对象不会关联 Monitor 锁,不遵循以上规则
5,Java 中的锁 Lock
1,Lock 接口
Java 中的锁 Lock 接口,比 synchronized 拥有更多,更细致的功能。
// java.util.concurrent.locks.Lock
public interface Lock {
// 获取锁,如果锁已被其他线程获取,则进行等待
void lock();
// 获取锁,可被 interrupt 打断
void lockInterruptibly() throws InterruptedException;
// 获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false
// 该方法无论如何都会立即返回,在拿不到锁时不会一直在那等待
boolean tryLock();
// 在拿不到锁时会等待一定的时间
// 在时间期限内如果还拿不到锁,就返回false。
// 如果一开始拿到锁或者在等待期间内拿到了锁,则返回true
// 该方法也是可打断的
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 条件变量,用于线程协作
Condition newCondition();
}
Lock.lock
方法使用模板:
lock.lock(); // 必须在 try 块之外,不能在里边
// lock 方法与 try 块之间最好不要有任何代码,以免出现异常,导致 unlock 无法执行
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
Lock.tryLock
方法使用模板:
if(lock.tryLock()) {
try{
// 获取到了锁,处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
// 不能获取到锁,则直接做其他事情
}
Lock.lockInterruptibly
方法使用模板:
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
// 获取锁时被打断
// e.printStackTrace();
return;
}
try {
// 获得了锁,do someThing()
} catch(Exception ex){
} finally {
lock.unlock();
}
2,ReentrantLock 类
ReentrantLock
(可重入锁) 类,是唯一实现了 Lock
接口的类。与 synchronized 一样,都支持可重入。
相对于 synchronized
它具备如下特点
- 可中断
lock()
方法是不可打断的,lockInterruptibly()
是可打断的
- 可以设置超时时间
- 可以设置为公平锁(防止线程饥饿,某些线程一直持有锁,某些线程一直得不到锁)
- 支持多个条件变量
其中的一些方法:
isLocked() //判断锁是否被任何线程获取了
isHeldByCurrentThread() //判断锁是否被当前线程获取了
hasQueuedThreads() //判断是否有线程在等待该锁
3,synchronized 与 Lock 对比
- synchronized 是隐式的锁,使用方便
- synchronized 不需要手动释放锁(不会出现死锁现象),而 Lock 需要开发者负责锁的释放,否则会导致死锁
- Lock 是显示的锁,性能更好
性能高低: Lock > synchronized 块 > synchronized 方法
4,ReadWriteLock 接口
ReadWriteLock
接口是 Java 中的读写锁接口:
public interface ReadWriteLock {
Lock readLock(); // 获取读锁
Lock writeLock(); // 获取写锁
}
读写锁的特点:
- 当一个线程已经占用了读锁
- 此时其他线程如果要申请读锁,则可以申请到
- 此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
- 当一个线程已经占用了写锁
- 此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁
ReentrantReadWriteLock
(可重入读写锁)类实现了 ReadWriteLock
接口。
读写锁特点:
- 读-读:可并发
- 读-写:互斥
- 写-写:互斥
当读操作远远高于写操作时,这时候使用读写锁 让读-读可以并发,提高性能。
使用方式:
ReentrantReadWriteLock rw = new ReentrantReadWriteLock(); // 读写锁
ReentrantReadWriteLock.ReadLock r = rw.readLock(); // 获取读锁
ReentrantReadWriteLock.WriteLock w = rw.writeLock(); // 获取写锁
// 读操作用读锁控制
public Object read() {
r.lock(); // 获取读锁
try {
return data;// 读取数据
} finally {
r.unlock(); // 释放读锁
}
}
// 写操作用写锁控制
public void write() {
w.lock(); // 获取写锁
try {
log.debug("写入操作");
} finally {
w.unlock(); // 释放写锁
}
}
注意事项:
- 读锁不支持条件变量
- 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
- 重入时降级支持:即持有写锁的情况下去获取读锁
5,锁的分类
锁的分类:
- 可重入锁:可重入性表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
- 像 synchronized 和 ReentrantLock 都是可重入锁
- 举个简单的例子,当一个线程执行到某个 synchronized 方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法 method2
- (假如synchronized不具备可重入性,此时线程A需要重新申请锁,这样就会线程A一直等待永远不会获取到)
- 可中断锁: 是指可以被中断的锁
- 在Java中,synchronized 不是可中断锁,而 Lock 是可中断锁
- 如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
lockInterruptibly()
的用法体现了Lock的可中断性。- 注意:
lock()
方法是不可打断的,lockInterruptibly()
是可打断的
- 读写锁:读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁,使得多个线程之间的读操作不会发生冲突。
- 公平锁:公平锁尽量以请求锁的顺序来获取锁
- 比如同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁
- 非公平锁即无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁
- 在Java中,
synchronized
就是非公平锁,它无法保证等待的线程获取锁的顺序 - 而对于
ReentrantLock
和ReentrantReadWriteLock
,它默认情况下是非公平锁,但是可以设置为公平锁- 比如
ReentrantLock lock = new ReentrantLock(true);
isFair()
方法判断锁是否是公平锁
- 比如
可重入锁的例子:
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {}
}
6,死锁
一个例子:
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
// 此代码将进入死锁
定位死锁的方法:
- 使用 jps 定位进程 id,再用 jstack 定位死锁
- 使用 jconsole工具
- 另外,如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过
top
先定位到 CPU 占用高的 Java 进程,再利用top -Hp 进程id
来定位是哪个线程,最后再用jstack
排查
jstack 的输出内容:
哲学家就餐问题是一个经典的死锁问题
有五位哲学家,围坐在圆桌旁
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子
- 如果筷子被身边的人拿着,自己就得等待
代码实现:
// 筷子类
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
// 哲学家类
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}
@Override
public void run() {
while (true) {
synchronized (left) { // 获得左手筷子
synchronized (right) { // 获得右手筷子
eat(); // 吃饭
} // 放下右手筷子
} // 放下左手筷子
}
}
}
// main 方法
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
程序执行一会就会死锁,可以使用 jconsole 工具检测死锁。
6,Java 线程通信
1,操作系统线程状态转换
- 初始状态:刚创建完线程对象,还未与系统线程关联
- 可运行状态 / 就绪转态:已与系统线程关联,可以由 CPU 调度执行
- 运行状态:获取了 CPU 时间片,线程正在运行中
- 阻塞状态:一般指阻塞与系统IO(文件/网络)
- 当 IO 完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 终止转态:线程已经执行完毕,生命周期已经结束
2,Java 线程状态转换
3,Java wait join 原理
wait 原理:
- 一开始小南正持有锁,由于条件不满足,小南不能继续进行计算
- 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
- 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋
- 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)
- 小南于是可以离开休息室,重新进入竞争锁的队列
wait/notify 原理
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
Join 原理:是调用者轮询检查线程 alive 状态
t1.join();
// 等价于下面的代码
synchronized (t1) {
// 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束
while (t1.isAlive()) {
t1.wait(0);
}
}
Java 中提供了几个方法(这些方法都在 Object
类中定义)来解决线程之间的通信问题:
wait()
:该方法使得线程(释放掉锁,处于等待状态)一直等待(阻塞),直到其它线程通知(notify)- 假如线程 A 持有了一个锁 lock 并开始执行,它可以使用 lock.wait() 让自己进入等待状态,这个时候,lock 这个锁是被释放了的。
- 当线程 B 获得了 lock 这个锁并开始执行,它可以在某一时刻,使用 lock.notify() ,通知之前持有 lock 锁并进入等待状态的线程 A。
- (注意:这时线程 B 并没有释放锁 lock,除非线程 B 使用 lock.wait() 释放锁,或者线程B执行结束自行释放锁,线程A才能得到 lock 锁。)
wait(long timeout)
:可指定等待时长notify()
:随机唤醒一个处于等待状态的线程notifyAll()
:唤醒同一个对象上所有调用 wait() 方法的线程,优先级高的线程优先调度
注意:
- 以上这些方法都是 Object 类中的方法,都只能在同步方法或者同步代码块中使用,否则会抛异常
- 等待/通知机制使用的是同一个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能用等待/通知机制通信的
一个例子:
public class TestCommunication {
private static final Object lock = new Object();
static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadA: " + i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadB: " + i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
7,Java 线程同步容器
ArrayList、LinkedList、HashMap
这些容器都是非线程安全的,如果有多个线程并发地访问这些容器时,就会出现问题。
1,Java 同步安全容器
Vector、Stack、HashTable
中的方法都进行了 synchronized
同步,是线程安全的, 因为有了 synchronized
,所以性能较低。
另外 java.util.Collections
类中的几个以 synchronizedXXX
开头的静态方法,也可以创建同步容器。
2,Java 并发容器
Java 并发容器 java.util.concurrent
包下的东西,这些比同步容器性能更好,更加安全。
ConcurrentHashMap
:并发的 HashMapCopyOnWriteArrayList
:并发的 ArrayList,写时复制的容器CopyOnWriteArraySet
:并发的 ArraySet,写时复制的容器ConcurrentLinkedQueue
:并发的 LinkedQueueConcurrentSkipListMap
:并发的 SkipListMap,可以在高效并发中替代 SoredMap(用Collections.synchronzedMap
包装的TreeMap)ConcurrentSkipListSet
:并发的 SkipListSet,可以在高效并发中替代 SoredSet(用Collections.synchronzedSet
包装的TreeMap)
CopyOnWrite 写时复制:
- 当往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy 复制出一个新的容器
- 然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器
- 这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器
CopyOnWrite 应用场景:
- CopyOnWrite 并发容器用于读多写少的并发场景;比如白名单,黑名单,商品类目的访问和更新场景
CopyOnWrite 的缺点:CopyOnWrite 容器有很多优点,但是同时也存在两个问题:
- 内存问题:会很占内存
- 数据一致性问题:
- CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性
- 所以如果你希望写入的的数据,马上能读到,则不要使用CopyOnWrite容器
3,Java 非阻塞队列
Java 非阻塞队列包括 java.util.PriorityQueue
和 java.util.LinkedList
。
对于非阻塞队列,一般情况下建议使用 offer、poll
和 peek
三个方法,不建议使用 add
和 remove
方法。
因为使用 offer、poll
和 peek
三个方法可以通过返回值判断操作成功与否,而使用 add
和 remove
方法却不能达到这样的效果。
注意,非阻塞队列中的方法都没有进行同步措施。
add(E e)
:将元素e插入到队列末尾- 如果插入成功,则返回true;
- 如果插入失败(即队列已满),则会抛出异常
remove()
:移除队首元素- 若移除成功,则返回true;
- 如果移除失败(队列为空),则会抛出异常
offer(E e)
:将元素e插入到队列末尾- 如果插入成功,则返回true
- 如果插入失败(即队列已满),则返回false
poll()
:移除并获取队首元素- 若成功,则返回队首元素
- 否则返回null
peek()
:获取队首元素- 若成功,则返回队首元素
- 否则返回null
4,Java 阻塞队列
阻塞队列会对当前线程产生阻塞:
- 比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素
- 当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒),这样提供了极大的方便性
阻塞队列包括了非阻塞队列中的大部分方法,但是要注意这些方法在阻塞队列中都进行了同步措施。
除此之外,阻塞队列提供了另外4个非常有用的方法:
put(E e)
向队尾存入元素,如果队列满,则等待take()
从队首取元素,如果队列为空,则等待offer(E e,long timeout, TimeUnit unit)
- 向队尾存入元素,如果队列满,则等待一定的时间
- 当时间期限达到时,如果还没有插入成功,则返回false;否则返回true
poll(long timeout, TimeUnit unit)
- 从队首取元素,如果队列空,则等待一定的时间
- 当时间期限达到时,如果取到,则返回null;否则返回取得的元素
在 java.util.concurrent
包下提供了若干个阻塞队列,主要有以下几个:
ArrayBlockingQueue
:基于数组实现的一个阻塞队列- 在创建
ArrayBlockingQueue
对象时必须制定容量大小 - 并且可以指定公平性与非公平性,默认情况下为非公平的(即不保证等待时间最长的队列最优先能够访问队列)
- 在创建
LinkedBlockingQueue
:基于链表实现的一个阻塞队列- 在创建
LinkedBlockingQueue
对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE
- 在创建
PriorityBlockingQueue
:以上2种队列都是先进先出队列,而PriorityBlockingQueue
却不是- 它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素
- 注意,此阻塞队列为无界阻塞队列,即容量没有上限,前面2种都是有界队列
DelayQueue
:基于PriorityQueue
,一种是延时阻塞队列,DelayQueue
中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue
也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
文章作者 @码农加油站
上次更改 2022-02-18