- 先看一段代码
public class MainActivity extends AppCompatActivity {
private static String TAG = "MainActivity";
private int count = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this,TestActivity.class);
startActivity(intent);
}
});
for (int i=0;i < 10;i++){
Thread thread = new Thread("Thread_"+i){
@Override
public void run(){
for (int j=0;j < 1000;j++){
count = count+1;
Log.d(TAG,Thread.currentThread().getName()+":"+count);
}
}
};
thread.start();
}
}
}
我们预测下这段代码的执行结果,也就是count的最终值。有人可能会说是10000。但是实际结果是小于等于10000的一个数。原因是
count = count+1;
是一个非原子操作,至少包含三个语句
- 从内存取出count的值
- 给count加1
- 写回内存
那极有可能存在这种情况
线程1从内存中读到count的值为1,此时线程2也读到了1,然后2个线程都给count做自增操作并写回内存,此时内存中count的值为2,并不是正确结果3。
- 那如何解决多线程并发导致不能获得正确结果的问题呢?
java引入了锁的机制,也就是关键字synchronized同步锁,每个对象都有一把独立的锁,类对象只有一个锁。只有获得锁,才能执行被synchronized修饰的代码块或者方法。
- synchronized修饰代码块
我们把上面的代码稍微修改下
for (int i=0;i < 10;i++){
final Thread thread = new Thread("Thread_"+i){
@Override
public void run(){
synchronized (MainActivity.this){
for (int j=0;j < 1000;j++){
count = count+1;
Log.d(TAG,Thread.currentThread().getName()+":"+count);
}
}
}
};
thread.start();
}
每个线程执行的时候,都尝试去获取MainActivity对象的锁,如果拿不到,就阻塞等。这就保证了同一时刻只有一个线程读写count这个变量,从而确保了结果的正确性,但是计算耗时增加了,效率变低。
修饰代码块很好理解,sychronized拿到的是实参对象的锁。
- 修饰成员方法
看一段代码
public class MainActivity extends AppCompatActivity {
private static String TAG = "MainActivity";
private int count = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Thread thread = new Thread("Thread_"+"test"){
@Override
public void run(){
try {
test();
}catch (Exception e){
e.printStackTrace();
}
}
};
thread.start();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
synchronized (MainActivity.class){
Log.d(TAG,"我获得了类锁");
}
}catch (Exception e){
e.printStackTrace();
}
}
});
thread1.start();
}
public synchronized void test() throws Exception{
while (true){
Thread.sleep(1000);
}
}
}
我们用sychrionized修饰了increase这个方法。如果这个sychrionized拿到的是类锁,那么
Log.d(TAG,"我拿到了类锁");
代码将永远得不到执行,但是实际情况是
01-02 18:03:49.760 14175-14203/com.debug.pluginhost D/MainActivity: 我获得了类锁
这行代码得到了执行,这说明synchronized修饰成员方法的时候,获得是对象的锁。
- 修饰静态方法
修饰静态方法我们就不举例了,这种情况,synchronized获得是类对象的锁。
- wait(),notify()和notifyAll()
synchrinized拿到锁后,如果需要,可以中途可以通过lockobj.wait()释放锁并阻塞在wait()处,等待其他线程通过lockobj.notify()或者lockobj.notifyAll()唤醒继续执行,一个典型的例子就是阻塞队列。
看代码
static class BlockQ{
ArrayList<Object> queue = new ArrayList<>();
int maxSize = 1;
public void push(Object o){
try {
synchronized (queue){
while (queue.size() >= maxSize){
queue.wait();
}
Log.d(TAG,"push :"+o);
queue.add(o);
queue.notify();
}
}catch (Exception e){
e.printStackTrace();
}
}
public Object pop(){
try {
synchronized (queue){
while (queue.size() == 0){
queue.wait();
}
Object o = queue.remove(0);
Log.d(TAG,"pop对象:"+o.toString());
queue.notify();
return o;
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
我们想象有两个集合CompetitionSet和WaitSet,我们简称为C和W,在C中的线程是有资格获取对象锁的,在W中的线程是没资格的,但是如果有人通知它们或者它们当中一个,它们就可以进入C集合,参加竞争锁。默认情况下,大家都在集合C,假设某个线程T获得锁,它开始执行代码,但是它发现自己无法处理当前的情况,因此只能等待,通过调用wait方法进入W集合,并释放自己拿到的锁
while (queue.size() == 0){ //我无法处理这种情况
queue.wait();//进入等待集合吧,并且释放了自己获得的锁
}
当有线程T1获得了锁,并成功执行push方法后,发出了notify()通知,此时JVM会从W中随机选一个线程T进入C集合去竞争锁,一旦T获得锁,它将从wait()处继续执行代码。如果能把notify弄明白,那么notifyAll就不难了,从字面意思就能理解,notifyAll会把W集合里所有的等待线程都放入到C集合里去竞争锁。
- 死锁
大家仔细考虑下上面的代码有什么问题?
我们假设有2个线程C1和C2调用pop方法,有1个线程P1调用push方法,假设执行顺序是这样的
- C1执行,发现queue里没数据,那么C1进入W集合
- C2执行,发现queue里没数据,那么C2进入W集合
- P1执行,push一个数据到队列后,唤醒C1线程,此时C集合里有C1和P1,如果此时P1再次获得对象锁,那么P1进入W集合,此时C集合里只剩下C1,queue.size = 1
- C1获得对象锁,pop数据后,唤醒了C2,此时C1、C2在集合C中,queue.size = 0
- C1获得对象锁,因为queue.size = 0,因此C1进入W集合
- C2获得对象锁,因为queue.size = 0,因此C2进入W集合
- 程序陷入死锁
思考notify和notifyAll的不同,在这种场景下只能用notifyAll。因为notifyAll会唤醒所有在W集合中的线程。