关于Java线程同步的基本知识
Java中线程同步,主要通过synchronized关键字,此关键字可用于修饰方法和代码块,在实际运行过程中表示对方法和对象上加锁,被加锁的方法仅能被一个线程同时调用,一个线程调用此方法的过程中,其他线程再调用此方法时会被等待,直到调用方法结束,用于代码块时也是类似,只有代码块的对象锁被释放时,才能执行代码块,否则线程将保持等待状态。
synchronized在不同应用方式下的加锁对象:
- 普通同步方法(使用实例调用的非静态方法),加锁对象为当前方法的实例对象;
- 静态同步方法(调用类的静态方法),加锁对象为当前类对象;
- 静态代码块,加锁对象为括号内的对象;
执行synchronized的方法和代码块时,会对相应的锁对象加锁,以阻止其他线程调用,这样就可能得出一个结论,当一个对象中有多个方法被synchronized修饰时,这个实例对象的多个方法同一时刻仅能被一个线程调用并运行,如果是静态方法被synchronized修饰时,则同一个类的多个静态同步方法在同一个JVM的(自定义ClassLoader加载类的情况特殊考虑)中同一时间仅能被一个线程调用并运行。
线程的阻塞与通知
在实际应用场景下,经常会碰到某些线程因为资源不具备而需要等待,此时就要释放锁让其他线程进行资源生产和准备的工作,在资源具备的条件再再唤醒当前线程,常规的思路是对加锁变量进行轮询,当资源具备时进行操作,不具备时休眠一定时间,这个休眠时间可以由开发人员指定,但这种方法有一个缺点,那就是休眠时间过长时响应不及时,休眠时间过短时占用系统资源过大,那能不能在持有锁的情况下暂时释放锁,等待条件具备时由其他线程唤醒当前代码的执行吗?
Object的wait方法
这就要用到Object.wait()方法了,Java给Object对象提供了wait、notify、notifyAll方法来实现线程的等待唤醒功能,经过查看JAVA源代码,发现这3个方法都是本地方法,且都是final方法,不能被重写,那这3个方法怎么用呢?
- wait方法:调用加锁对象的wait方法时可以阻塞当前线程的运行,直至其他线程调用本对象的nofity方法时可以唤醒本线程并继续执行剩余的代码;
- notify方法:调用加锁对象的notify方法时,可以唤醒等待这个对象的其他线程(即调用了加锁对象wait方法的线程),如果有多个线程正在等待,则只能唤醒一个线程,具体唤醒哪个线程就要看JVM的线程调度了
- notifyAll方法:与notify方法类似,只是在调用时可以唤醒所有等待这个对象的线程,至于哪个线程可以得到加锁对象的锁,就要看开发人员对程序逻辑的控制了
需要注意的地方
- 执行完notify和notifyAll方法后,并不会立即释放锁,需要等到synchronized块的结束或者方法执行完成后才会释放锁,其他线程才能获得锁并继续执行代码。
- 这3个方法的调用均需要在synchronized关键字所修饰的方法或代码块中,而且要保证调用的是当前加锁对象的方法,否则会抛出异常
例子
package test;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.Queue;
public class ObjectWaitTest {
public static Queue<String> queue = new LinkedList<String>();
public static void main(String[] args) {
Producer producer = new Producer();
new Consumer().start();
new Consumer().start();
producer.start();
}
//生产线程
static class Producer extends Thread{
@Override
public void run() {
setName("Producer Thread");
println("生产者线程已启动");
int i=0;
while(i++<6){
synchronized (queue) {
String product = "product "+sdf.format(new Date());
println("生产产品"+product);
queue.add(product);
println("通知消费者来消费");
queue.notify();
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
println("生产者释放锁");
}
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费线程
static class Consumer extends Thread{
public Consumer(){
this.setDaemon(true);//设置为守护线程,当所有非守护线程结束后,进程结束
}
static int idx = 1;
int myidx;
@Override
public void run() {
setName("Consumer Thread");
myidx = idx++;
println("消费者"+myidx+" 线程已启动");
while(true){
synchronized (queue) {
try {
queue.wait();//等待通知,有数据时才执行下面语句
} catch (InterruptedException e1) {
e1.printStackTrace();
}
String product = queue.poll();
println("消费者"+myidx+" 消费产品"+product);
}
}
}
}
static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
static private void println(String string) {
System.out.println(sdf.format(new Date())+" "+string);
}
}
输出的结果:
08:32:43 消费者1 线程已启动
08:32:43 消费者2 线程已启动
08:32:43 生产者线程已启动
08:32:43 生产产品product 08:32:43
08:32:43 通知消费者来消费
08:32:44 生产者释放锁
08:32:44 消费者1 消费产品product 08:32:43
08:32:45 生产产品product 08:32:45
08:32:45 通知消费者来消费
08:32:46 生产者释放锁
08:32:46 消费者2 消费产品product 08:32:45
08:32:47 生产产品product 08:32:47
08:32:47 通知消费者来消费
08:32:48 生产者释放锁
08:32:48 消费者1 消费产品product 08:32:47
08:32:49 生产产品product 08:32:49
08:32:49 通知消费者来消费
08:32:50 生产者释放锁
08:32:50 消费者2 消费产品product 08:32:49
08:32:51 生产产品product 08:32:51
08:32:51 通知消费者来消费
08:32:52 生产者释放锁
08:32:52 消费者1 消费产品product 08:32:51
08:32:53 生产产品product 08:32:53
08:32:53 通知消费者来消费
08:32:54 生产者释放锁
08:32:54 消费者2 消费产品product 08:32:53
结论
由此可见,当消费线程不知道何时才会具备资源时,可以调用加锁对象的wait方法交出加锁对象的锁,让生产线程去生产资源,等待生产线程生产资源后,可以调用加锁对象的notify线程,这个例子中是每次生产一个资源,所以调用notify方法即可,如果每次生产多个资源,而同时又有多个消费线程时,可以调用notifyAll方法,通知所有消费线程去消费产品,此时也要对消费线程进行改造,获取资源时增加判断,如果没有资源时继续下一个循环并调用加锁对象的wait方法进行阻塞等待状态。