接上文:
线程间的共享
单个、孤立的线程是没有价值的,多个线程只有在协同处理、共享数据、相互配合的情况下完成工作,才能体现多线程编程的巨大价值。
synchronized是Java内置的语言锁,synchronized关键字修饰了的方法和同步块可以确保多个线程在同一时刻只有一个线程处于方法个同步块中。
synchronized修饰的方法和块锁的是类的某一个对象,针对的是不同线程的同一个对象。
加了static synchronized的方法和块锁的是类,针对的是不同线程的这个类的所有对象。不同的对象锁互不干扰,但由于一个类的class对象只有一个,类锁也只有一个(官方虽然没有类锁的概念,但这样好理解我们就这样叫)。
对象锁大家很熟悉,今天只看一个类锁的例子:
public class SynDemo {
String name;
public SynDemo(String name) {
this.name = name;
}
public void synStaticCount(String name) {
for (int i = 0; i < 10; i++) {
System.out.println(name + "------" + i);
}
}
public static class StaticSync extends Thread {
private SynDemo demo;
public StaticSync(SynDemo demo) {
this.demo = demo;
}
@Override
public void run() {
super.run();
System.out.println(demo.name + "--------------线程启动");
demo.synStaticCount(demo.name);
}
}
public static void main(String[] args) throws InterruptedException {
//不同的进程 不同的SynDemo对象去访问SynDemo类中的synStaticCount方法
new StaticSync(new SynDemo("demo1")).start();
new StaticSync(new SynDemo("demo2")).start();
new StaticSync(new SynDemo("demo3")).start();
new StaticSync(new SynDemo("demo4")).start();
}
}
打印结果---首先不做同步处理的情况下
我们把synStaticCount方法加上synchronized 对象锁
public synchronized void synStaticCount(String name) {
for (int i = 0; i < 10; i++) {
System.out.println(name + "------" + i);
}
}
打印结果如下---没有实现同步
最后使用类锁
public static synchronized void synStaticCount(String name) {
for (int i = 0; i < 10; i++) {
System.out.println(name + "------" + i);
}
}
打印结果如下--实现了同步的访问
可以看到不同的线程、不同的SynDemo对象访问加了类锁的方法实现了同步。
线程间的协作
生产者、消费者:一个线程修改了一个对象的值,另一个线程接收到了变化,进行自己的操作。前者是生产者,后者是消费者。这种方法需要消费者线程不停的对象的变化是否满足做相应操作的条件,不能确保及时性且消耗太大。
等待/通知机制:线程A通过对象的wait()方法进入等待状态,另一个线程通过调用该对象的notify()或notifyAll()方法,通知线程A从该对象的wait()方法返回,执行后续的操作。
等待线程:必须持有对象的锁,收到信号后要检查是否满足条件,不满足则继续等待,满足条件才会执行后续操作。
synchronized (对象){
while (条件不满足){
对象.wait();
}
后续操作
}
通知线程:获取对象锁,改变条件
synchronized (对象){
改变条件
对象.notifyAll();
}
等待方对象调用wait()后即释放锁,再从wait()方法返回之前,所在线程和其他线程重新竞争对象锁,通知方调用notifyAll()后释放锁,获取到对象锁的线程会从wait()方法返回,继续执行后续操作,在它执行完synchronized代码块释放锁,剩下被唤醒的线程会继续竞争锁,直到都执行完毕。
notify()和notifyAll():notifyAll()唤醒的是所有线程,notify()唤醒的是某一个线程,至于最终唤醒哪一个不能控制,所以通常情况下使用notifyAll()。
ThreadLocal
线程隔离。不同的线程在使用ThreadLoca包含的变量,会创建该线程独有的实例副本,且该副本只有当前线程能够使用。ThreadLocal变量通常用 private static修饰。当一个线程结束时,该线程持有的所有ThreadLocal变量的副本都会被回收。
ThreadLocal适用于每个线程需要独立的实例,该实例需要在多个方法和线程中被使用。
static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 1;
}
};
Lock
synchronized是Java语言层面的内置锁,获取和释放锁的过程都是不可见、不可控的,最重要的是在获取锁的过程中是不能中断的。
Lock也是一种显示锁,它是语法层面的,获取和释放锁可以手动调用API,相比于synchronized它还多了一个尝试获取锁的机制,tryLock()可以在获取锁的过程中可中断。
private Lock lock = new ReentrantLock();
public void fun() {
lock.lock();
try {
// TODO
} finally {
lock.unlock();
}
}
由于lock的锁需要手动去释放,在释放前如果出现异常会出现锁得不到释放,所以要加finally,在finally释放锁。
锁的公平与非公平
synchronized是非公平锁,Lock默认是非公平锁,构造方法里传true是公平锁。
当A、B、C三个线程申请同一个锁,A先来并且获取了锁,B随后来,这是A持有锁,B就只能挂起,当A执行完释放了锁,这时C来了,如果CPU让最后来的C获取了锁,这是非公平锁。如果CPU按先后顺序,把后来的C挂起,让B结束挂起然后获取锁,这叫公平锁。
线程挂起也就是让出当前CPU消耗一次“上下文切换”,结束挂起又消耗一次“上下文切换”,上一篇说个一次“上下文切换”需要20000CPU时间片,如果使用公平锁,明显要比非公平锁效率更低。
关于线程的笔记就写到这。