线程间的共享
线程运行拥有自己的栈空间,按既定的代码一步一步地执行,直到终止。如果每个线程仅仅只是孤立的运行,价值会很少,如果多个线程能够相互配合完成工作,如数据间的共享,协同处理事情,这样会带来巨大的价值。但是多个线程"同时"操作一个公共对象就会存在线程间安全问题。
synchronized
Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式进行使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,保证了线程对变量的可见性和排他性,又称为内置锁机制。
对象锁和类锁
对象锁用于对象实例方法,或一个对象实例上,类锁用于类的静态方法或类的class对象上。
public class SynTest {
private static long count = 0;
private Object lock = new Object();//作为一个锁
private Object sLock = new Object();
/*****对象锁*****/
/**
* 锁非静态变量
* 锁住同一变量的方法共享同一把锁
*/
public void incCount3() {
synchronized (lock) {
count++;
}
}
/**
* 锁this
* this就是当前对象实例,所有使用synchronized (this)共享同一把锁
*/
public void incCount1() {
synchronized (this) {
count++;
}
}
/**
* 锁非静态方法
* 等价于锁this
*/
public synchronized void incCount2() {
count++;
}
/*****类锁*****/
/**
* 锁静态变量
* 静态变量和类信息一样也是存在方法区并且整个JVM只有一份,所以加在静态变量上可以达到类锁的目的。
*/
public void incCount4() {
synchronized (sLock) {
count++;
}
}
/**
* 锁静态方法
* 静态方法也是存在方法区并且整个JVM只有一份,所以加在静态变量上可以达到类锁的目的。
*/
public static synchronized void incCount5() {
count++;
}
/**
* 锁Class
*/
public void incCount6() {
synchronized (SynTest.class) {
count++;
}
}
//线程
private static class Count extends Thread {
private SynTest synTest;
public Count(SynTest synTest) {
this.synTest = synTest;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synTest.incCount1();//count = count+1000
}
}
}
public static void main(String[] args) throws InterruptedException {
SynTest synTest = new SynTest();
//启动两个线程
Count count1 = new Count(synTest);
Count count2 = new Count(synTest);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(synTest.count);//2000
}
}
volatile
最轻量的同步机制,volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了这个变量的值,这个新值对其他线程是立即可见的。
public class VolatileTest {
private volatile static boolean stop = false;//任务是否停止
private static class WorkThread extends Thread {
@Override
public void run() {
System.out.println("Work start...");
while (!stop) ;//无限循环
System.out.println("Work end");
}
}
public static void main(String[] args) throws InterruptedException {
new WorkThread().start();
Thread.sleep(1000);
stop=true;
System.out.println("Main end");
}
}
不加volatile时,WorkThread线程无法感知主线程修改了stop的值,从而不会退出循环。而加了volatile后,WorkThread线程可以感知主线程修改了stop的值立即退出循环。
但是volatile不能保证数据在多个线程下同时写时的安全性。
public class VolatileNoSafeTest {
private volatile long count = 0;
public void incCount() {
count++;
}
//线程
private static class Count extends Thread {
private VolatileNoSafeTest noSafeTest;
public Count(VolatileNoSafeTest noSafeTest) {
this.noSafeTest = noSafeTest;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
noSafeTest.incCount();//count = count+10000
}
}
}
public static void main(String[] args) throws InterruptedException {
VolatileNoSafeTest noSafeTest = new VolatileNoSafeTest();
//启动两个线程
Count count1 = new Count(noSafeTest);
Count count2 = new Count(noSafeTest);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(noSafeTest.count);//20000
}
}
所以volatile适用场景:一个线程写,多个线程读
ThreadLocal
ThreadLocal和synchronized都是用于解决多线程并发访问。synchronized是利用的锁机制,使变量或者代码块在某一个时刻只能被一个线程访问(线程等待,牺牲时间解决冲突)。而ThreadLocal是为每一个线程提供了变量的副本,使得每个线程在某一时间访问的不是同一个对象,达到了隔离多个线程对数据的数据共享(牺牲空间解决冲突)。
/**
*类说明:演示ThreadLocal的使用
*/
public class UseThreadLocal {
//ThreadLocal变量,每个线程都有一个副本,互不干扰
//存放Integer类型的对象,对它的读写操作都是线程安全的。
private static ThreadLocal<Integer> threadLocal
= new ThreadLocal<Integer>(){
//这个initialValue()方法是一个延迟调用的方法,在线程第一次调用set或get时才初始化,并且仅执行一次,缺省则返回null。
@Override
protected Integer initialValue() {
return 1;
}
};
/**
* 运行3个线程
*/
public void StartThreadArray(){
Thread[] runs = new Thread[3];
for(int i=0;i<runs.length;i++){
runs[i]=new Thread(new TestThread(i));
}
for(int i=0;i<runs.length;i++){
runs[i].start();
}
}
/**
*类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
*/
public static class TestThread implements Runnable{
private int id;
public TestThread(int id){
this.id = id;
}
public void run() {
System.out.println(Thread.currentThread().getName()+":start");
//获取当前线程所对应的线程局部变量
Integer count = threadLocal.get();
count = count+id;
//设置当前线程的线程局部变量的值
threadLocal.set(count);
System.out.println(Thread.currentThread().getName()
+":"+ threadLocal.get());
//将当前线程局部变量的值删除,目的是为了减少内存的占用。当线程结束后,对应线程的局部变量将自动被垃圾回收器回收,所以非必须,但是可以加快内存回收的速度。
//threadLocal.remove();
}
}
public static void main(String[] args){
UseThreadLocal test = new UseThreadLocal();
test.StartThreadArray();
}
}
源码浅析
//ThreadLocal.java
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//调用getMap获取线程对应的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//调用getMap获取线程对应的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//存在map就set,不存在则创建mao并set
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
//Thread中维护了一个ThreaLocalMap
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreaLocalMap,并赋值给线程成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//Thread.java
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
可以看到,每个线程持有一个ThreadLocalMap对象。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//key、value结构,key就是ThreadLocal,value是需要隔离的线程
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
//用数组保存Entry,因为可能有多个变量需要线程隔离访问
private Entry[] table;
//...
}
![](https://img.haomeiwen.com/i6345209/ceb929752899e697.png)
小结:ThreadLocal如何做到线程隔离呢?每个Thread维护了一个自己独有的ThreadLocalMap,get与set都是根据同一个ThreadLocal实例去自己的ThreadLocalMap里面找,互不干扰。
ThreadLocal内存泄漏问题
ThreadLocal引用链如图所示:
![](https://img.haomeiwen.com/i6345209/b7390d3fa7c49469.png)
ThreadLocal在外部没有强引用时,发生GC时会被回收,那么ThreadLocal中保存的key值就会变为null,也就没办法访问这些key为null的所对应的Entry中的value了,而Entry又被ThreadLocalMap对象引用,ThreadLocalMap对象又被Thread对象所引用,如果Thread一直不结束的话,就会一直存在一条强引用链:ThreadLocal Ref—>Thread—>ThreadLocalMap—>Entry—>value,而value永远不会被访问到,所以就存在了内存泄漏。
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期根Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal如何避免内存泄漏:在使用完ThreadLocal变量后,手动调用remove,防止ThreadLocalMap中Entry一直保持对value的强引用,导致value不能被回收。
线程间的协作
等待/通知机制
是指一个线程A调用了对象O的wait()方法进入等待状态,另一个线程B调用了对象O的notify()或notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
wait( )和notify( )/notifyAll( )都不属于Thread类,而是属于Object基础类,也就是每个对象都有wait( )和notify( )/notifyAll( )的功能,因为每个对象都有锁。
- notify()
通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该对象获得了对象的锁,没有获得锁的线程重新进入WAITING状态。 - notifyAll()
通知所有等待在该对象上的线程 - wait()
调用该方法的线程进入WAITLING状态,只有等待另外线程的通知或被中断才会返回。调用wait()方法后,会释放对象的锁。 - wait(long)
超时等待一段时间,这里的参数时间是毫秒,也就是等待n毫秒,如果没有就通知就超时返回。 - wait(long,int)
对于超时时间更细粒度的控制,可以达到纳秒。
等待和通知的标准范式
等待方遵循如下规则:
synchronized (对象){
while (条件不满足){
对象.wait();
}
对应的处理逻辑
}
1)获取对象的锁
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
3)条件满足则执行对应的逻辑
通知方遵循如下规则:
synchronized (对象){
改变条件
对象.notifyAll();
}
1)获取对象的锁
2)改变条件
3)通知所有等待在对象上的线程
/**
*类说明:快递实体类
*wait和notify/notifyAll使用示例
*/
public class Express {
public final static String CITY = "ShangHai";
private int km;//快递运输里程数
private String site;//快递到达地点
public Express() {
}
public Express(int km, String site) {
this.km = km;
this.site = site;
}
/* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
public synchronized void changeKm(){
this.km = 101;
notify();
}
/* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
public synchronized void changeSite(){
this.site = "BeiJing";
notifyAll();
}
/*线程等待公里的变化*/
public synchronized void waitKm(){
while(this.km<100){
try {
wait();
System.out.println("Check Site thread["
+Thread.currentThread().getId()
+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the Km is "+this.km);
}
/*线程等待目的地的变化*/
public synchronized void waitSite(){
while(this.site.equals(CITY)){//快递到达目的地
try {
wait();
System.out.println("Check Site thread["+Thread.currentThread().getId()
+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the site is "+this.site);
}
}
/**
*类说明:测试wait/notify/notifyAll
*/
public class TestWN {
private static Express express = new Express(0,Express.CITY);
/*检查里程数变化的线程,不满足条件,线程一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*检查地点变化的线程,不满足条件,线程一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){
new CheckSite().start();
}
for(int i=0;i<3;i++){
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm();//快递公里数变化
}
}
在调用wait、notify/notifyAll方法之前,一定要对竞争资源进行加锁,即只能在同步方法或者同步块中调用wait、notify/notifyAll方法。
假设有三个线程执行了obj.wait( ),那么obj.notifyAll( )则能全部唤醒tread1,thread2,thread3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,tread1,thread2,thread3只有一个有机会获得锁继续执行,例如tread1,其余的需要等待thread1释放obj锁之后才能继续执行。
当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此,thread1,thread2,thread3虽被唤醒,但是仍无法获得obj锁。直到调用线程退出synchronized块,释放obj锁后,thread1,thread2,thread3中的一个才有机会获得锁继续执行。
尽可能用 notifyAll(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我 们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。
yield() 、sleep()、wait()、notify()/notifyAll()等方法对锁有何影响?
- yield()、sleep()被调用后,都不会释放当前线程所持有的锁。
- wait()被调用后,会释放当前线程持有的锁,而且当前线程被唤醒后,会重新去竞争锁,竞争到了锁之后才会执行wait方法后面的代码。
- notify()/notifyAll()被调用后,对锁无影响,线程只有在synchronized同步代码执行完后才会释放锁,所以notify()/notifyAll()一般都是synchronized同步代码的最后一行。
网友评论