公号:码农充电站pro

主页:https://codeshellme.github.io

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
  • 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:获取线程 ID
  • getName:获取线程名
  • setName:设置线程名
  • interrupt:打断线程
    • 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记
    • 如果打断的正在运行的线程,则会设置打断标记 ,但并不会影响被打断线程的运行,被打断线程可以根据打断标记isInterrupted),去进行下一步处理(比如停止运行)
    • park 的线程被打断,也会设置打断标记
  • 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 方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true
  • isDone 方法表示任务是否已经完成,若任务完成,则返回true
  • get() 方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回
  • 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

FutureTaskFuture 接口的一个唯一实现类。

示例:

// 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 就是非公平锁,它无法保证等待的线程获取锁的顺序
    • 而对于ReentrantLockReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁
      • 比如 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:并发的 HashMap
  • CopyOnWriteArrayList:并发的 ArrayList,写时复制的容器
  • CopyOnWriteArraySet:并发的 ArraySet,写时复制的容器
  • ConcurrentLinkedQueue:并发的 LinkedQueue
  • ConcurrentSkipListMap:并发的 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.PriorityQueuejava.util.LinkedList

对于非阻塞队列,一般情况下建议使用 offer、pollpeek 三个方法,不建议使用 addremove 方法。

因为使用 offer、pollpeek 三个方法可以通过返回值判断操作成功与否,而使用 addremove 方法却不能达到这样的效果。

注意,非阻塞队列中的方法都没有进行同步措施。

  • 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 也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。