同步和锁定
Java中每个对象都有一个内置锁。
1、synchronize
当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。
一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。
释放锁是指持锁线程退出了synchronized同步方法或代码块。
关于锁和同步的几个要点
- 只能同步方法,而不能同步变量和类;
- 每个对象只有一个锁;当提到同步时,应该清楚在哪个对象上同步。
- synchronized 锁的是对象,在应用数据共享时,要保证被锁的对象不要发生变化。
- 如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
- 如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
- 线程睡眠时,它所持的任何锁都不会释放。
- 线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。synchronized (this) 和 用synchronized修饰方法 有相同的效果,例如:
public int add(int m) {
synchronized (this) {
n = m + n;
}
return n;
}
等同于
public synchronized int add(int m) {
n = m + n;
return n;
}
静态方法同步
同步静态方法,需要一个类对象的锁,即Class对象
public static int add(int m) {
synchronized (Test.class) {
n = m + n;
}
return n;
}
等同于
public static synchronized int add(int m) {
n = m + n;
return n;
}
什么情况下,线程无法获得锁?
当线程A试图调用同步方法或同步代码块,要清楚这个同步是在哪个对象上的锁,如果此时锁被线程B占用,那么线程A在该对象上被阻塞,等待,直到锁被释放,线程A再次变为可运行或运行状态。
- 对于非静态的同步方法,当然是,调用同一个对象的同步方法会被阻塞,调用不同对象的同步方法不会被阻塞
- 对于静态的同步方法,都是在Class对象上加锁,调用它的同步方法当然会彼此阻塞
下面的例子可以帮助理解。两个不同的线程thread1和thread2,想同时操作Worker里面的index
public static void main(String[] args) throws InterruptedException {
Worker worker = new Worker(5);
// 下面两个线程用的是同一个对象worker
Thread thread1 = new Thread(worker);
Thread thread2 = new Thread(worker);
thread1.start();
thread2.start();
}
private static class Worker implements Runnable {
private Integer index;
public Worker(Integer index) {
this.index = index;
}
@Override
public void run() {
// 这里的this,表示当前对象,不同的实例,this指代的对象是不同的
synchronized (this) {
index++;
System.out.println(Thread.currentThread().getName() + "-------" + index);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-------" + index + ", END");
}
}
}
synchronized (this),即对Worker对象加锁,打印结果如下:
Thread-0-------6
Thread-0-------6, END
Thread-1-------7
Thread-1-------7, END
上面new Thread用的是同一个worker对象,那么针对两个线程,一个获得对象锁后,另一个必然阻塞。
2、volatile关键字
- 保证了不同线程对这个变量进行操作时的可见性。
即一个线程修改了某个变量的值, 这新值对其他线程来说是立即可见的。 - 不保证原子性
即不能保证数据在多个线程下同时写时的线程安全
volatile最适用的场景: 一个线程写, 多个线程读
一个线程在修改某个变量的值,其他线程来读取该变量的值都是实时可见的。
3、ThreadLocal
1. 介绍
规避线程不安全的方法,除了加锁之外,还可以使用ThreadLocal 。
如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。可理解为:线程局部变量。
2. ThreadLocal使用
public class UseThreadLocal {
// int型ThreadLocal变量
private static ThreadLocal<Integer> intLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 100;
}
};
// String型ThreadLocal变量
private static ThreadLocal<String> stringThreadLocal;
// 开启线程
public void StartThread(){
Thread thread1 = new Thread(new TestRunnable(1));
thread1.start();
Thread thread2 = new Thread(new TestRunnable(2));
thread2.start();
}
public static class TestRunnable implements Runnable{
public int id;
public TestRunnable(int id){
this.id = id;
}
public void run() {
System.out.println(Thread.currentThread().getName()+", intLocal.get:" + intLocal.get());
Integer i = intLocal.get();
i = i + id;
intLocal.set(i);
System.out.println(Thread.currentThread().getName() +", after set:"+ intLocal.get());
// ThreadLocal变量不再使用时,须remove
intLocal.remove();
}
}
public static void main(String[] args){
UseThreadLocal test = new UseThreadLocal();
test.StartThread();
}
}
输出结果:
Thread-0, intLocal.get:100
Thread-1, intLocal.get:100
Thread-0, after set:101
Thread-1, after set:102
3. ThreadLocal的实现原理
(1) Thread类
主要看它的成员变量 threadLocals
- 类型:ThreadLocal.ThreadLocalMap(类似于HashMap),key是ThreadLocal,value是set的值
- 初始化:此变量初始为null,只有调用ThreadLocal.set或get方法时,才会创建它
- 赋值:ThreadLocal.set()、get()、remove()
- 作用:存放该线程的ThreadLocal类型的本地变量
- 注意:不使用本地变量时,需要调用remove方法将此线程在threadLocals中的本地变量删除
(2)ThreadLocal类
这里看下set()、get()、remove()方法源码
set()
public void set(T value) {
// (1) 获取当前线程(调用者线程)
Thread t = Thread.currentThread();
// (2) 获取Thread的成员变量threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
// (3) 直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为value
map.set(this, value);
} else {
// (4) 当map为null,说明首次添加,需要首先创建出对应的map
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
// 获取Thread的成员变量threadLocals
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
// 创建threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
T result = (T)e.value;
return result;
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue(); // new ThreadLocal时会重写initialValue()进行赋值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap
ThreadLocalMap有一个内部类Entry,有一个Entry[]类型的变量,这个数组可以保存多个Entry对象。
值得注意的是,Entry持有ThreadLocal对象的弱引用。
弱引用:只要GC时,弱引用就会被回收。
因此,当GC时,Entry指向的ThreadLocal对象会被回收,那么Entry的key不在了,其value永远不会被访问,内存将暴增。因此,对于不再使用的线程本地变量,应及时remove。