关键字锁的作用用法:
可见性、有序性、原子性。
可见性,当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新的值。
另外synchronized也会保证可见性。
有序性,我们通过关键字volatile确保指令不会重排。
原子性,明为volatile,对它们的操作就会变成原子级别的。但这有一定的限制。例如,下面的例子中的n就不是原子级别的:
private volatile int n = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 100; j++) {
n++;
Log.d("" + Thread.currentThread().getName(), "run: " + n);
}
}
}, "线程" + i).start();
}
}
如果对n的操作是原子级别的,最后输出的结果应该为n=1000,而在执行上面积代码时,很多时侯输出的n都小于1000,这说明n++不是原子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作
当变量的值由自身的上一个决定时,如n++,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。
Java中的原子操作包括:
1)除long和double之外的基本类型的赋值操作
2)所有引用reference的赋值操作
3)java.concurrent.Atomic.* 包中所有类的
可以使用synchronized来代替volatile。
死锁的四大必要条件:
互斥条件
请求与保持条件
不剥夺条件
循环等待条件
2. 是什么类型的锁
(1) 公平锁或非公平锁(下面案例中有实际代码,自己执行一遍更容易理解)
a. 公平锁:先来的线程先执行,排成排按顺序。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
b. 非公平锁:后来的线程有可能先执行,可插队不一定按顺序。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
(2) 互斥锁
一次只能执行一个线程。
(3) 可重入锁
同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
可重入锁的例子:假如景区有3个收费项目,如果每个项目单独玩都需要收费。但是你买了个VIP。进入景区大门的时候工作人员为了方便,直接给你挂了个牌子,里面三个项目的工作人员看到你的牌子,就认为你已经买过该项目的门票,不在向你收费,进去就行。
一、同步机制synchronized
synchronized是java中解决并发问题的一种最常用最简单的方法,它可以确保线程
互斥的方法。
对于被synchronized修饰的代码块,如果A线程执行结束,会强制刷新线程缓存内容,通知其他synchronized修饰的线程值无效,需要重新读取(这点和volatile很相似)。
Java中每个对象都可以作为锁,,这是synchronized实现同步的基础:
1.普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁。
2.静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁。
3.同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
对应为方法,对象,代码块。
二、Lock
Lock是java.util.concurrent.locks包下的接口,Lock提供了的方法与语句可更广泛的锁定操作。
Lock提供的api有:
void lock()
lock() 获取锁,若是当前锁为不可用状态,则一直等待,线程休眠,直到获取到锁为止。
void lockInterruptibly()
lockInterruptibly() 获取锁,若是当前锁为不可用状态,当前线程禁用休眠,直到当前线程获取到锁或者其他线程中断当前线程,并且支持中断获取锁。
boolean tryLock()
tryLock() 获取锁,若是当前锁可用返回true,并获取锁,当前锁不可用直接返回false
boolean tryLock(long time, TimeUnit unit)
tryLock(long time, TimeUnit unit) 获取锁,如果锁在给定的等待时间内是空闲的,并且当前线程没有被中断,则获取锁。 如果锁可用,则此方法会立即返回值true。如果锁不可用,则出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到发生以下三种情况之一:
锁是由当前线程获取或其他线程中断当前线程,支持中断锁获取或指定的等待时间已过。
1.如果获取了锁,则返回值true。
2 .如果当前线程: 在进入该方法时设置其中断状态或在获取锁的同时被中断并且支持锁获取的中断,则抛出InterruptedException,并清除当前线程的中断状态。
3.如果经过了指定的等待时间,则返回值false。如果时间小于或等于零,则该方法根本不会等待。
void unlock();
unlock() 释放锁,
Condition newCondition();
newCondition() 获取Condition,Condition是Java提供了来实现等待/通知的类,Condition类还提供比wait/notify更丰富的功能,Condition对象是由lock对象所创建的。但是同一个锁可以创建多个Condition的对象,即创建多个对象监视器。
和wait类提供了一个最长等待时间,awaitUntil(Date deadline)在到达指定时间之后,线程会自动唤醒。但是无论是await或者awaitUntil,当线程中断时,进行阻塞的线程会产生中断异常。Java提供了一个awaitUninterruptibly的方法,使即使线程中断时,进行阻塞的线程也不会产生中断异常。
二、ReenTrantLock
ReentrantLock(翻译过来叫可重入锁)是基于AQS进行的实现。
ReentrantLock是一种基于AQS(Abstract Queued Synchronizer)框架的应用实现,是JDK中一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。
ReentrantLock(fair: Boolean)是否为公平锁。
ReentrantLock底层是通过 compareAndSwap
关于ReentrantLock的使用很简单,只需要显示调用,获得同步锁,释放同步锁即可。
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
.....................
lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
try {
//操作
} finally {
lock.unlock(); //释放锁
}
四、CountDownLatch
import java.util.concurrent.CountDownLatch;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
public class Test2Activity extends Activity {
private CountDownLatch mCountDownLatch;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test_layout);
mCountDownLatch = new CountDownLatch(3);
new Thread(new Runnable() {
@Override
public void run() {
try {
Log.d("TAG", "Thread Main A start:"+mCountDownLatch.getCount());
mCountDownLatch.await();
Log.d("TAG", "Thread Main A end:"+mCountDownLatch.getCount());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Log.d("TAG", "Thread Main B start:"+mCountDownLatch.getCount());
mCountDownLatch.await();
Log.d("TAG", "Thread Main B end:"+mCountDownLatch.getCount());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
mCountDownLatch.countDown();
Log.d("TAG", "Thread A:"+mCountDownLatch.getCount());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
mCountDownLatch.countDown();
Log.d("TAG", "Thread B:"+mCountDownLatch.getCount());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
mCountDownLatch.countDown();
Log.d("TAG", "Thread C:"+mCountDownLatch.getCount());
}
}).start();
}
CountDownLatch 通过它的名字也能猜出一二来,Countdown 顾名思义倒计时,Latch可以理解为触发或者发射。也就是说当倒数到0时就可以发射火箭啦,在线程中就是一个等待的线程,当 countdown 到 0 就不用再等待了,可以向下执行任务了。
上面分析了一下 CountdownLatch 的概念,相信大家也能体会它的使用场景了。在多个线程中,如果某个线程需要等待其他几个线程执行某个操作后,才能向下执行动作的话,我们就可以使用 CountDownLatch 来实现多线程之间的同步。
方法
public CountDownLatch(int count)
构造函数,需传入计数的总数。
public void await()
阻塞线程,直到总计数值为0
public void countDown()
当前总计数减一
public long getCount()
当前总计数
通过上面 CountDownLatch 提供的几个方法,我们可以看出来 CountDownLatch 没有重新设置总计数的方法,也就是说这个类只有初始化的时候才设置一次,等计数完成后就不可再复用,需要重新创建一个新的 CountDownLatch 才可以再计数。
CountDownLatch 还有一个超时等待的 await 函数,这里就不多分析了。CountDownLatch 几个重要的方法我们已经知道了,现在来看看怎么使用这几个方法。
CountDownLatch使用简单,直接是需要等待的数量。然后每个都await,然后再通过countDown每次减1,最后为0时,直接发射。