美文网首页
synchronized初学笔记

synchronized初学笔记

作者: 巫师Android | 来源:发表于2020-09-28 15:20 被阅读0次

我们首先来看一个例子:
【demo1】

public class SynchronizedTest3 implements Runnable {
    public static int count = 0;

    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            count++;
        }
    }
}

public class MainActivityTest {

    @Test
    public void synchronizedTest3(){
        for (int i = 0 ; i < 10 ; i++){
            Thread thread = new Thread(new SynchronizedTest3());
            thread.start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + SynchronizedTest3.count);
    }
}

这个例子的结果result是:


image.png image.png

每次都不一样。
在此例中,我们开启了10个线程,每个线程都累加100000次,预期结果是10*100000=1000000。可运行多次都不是这个数,并且每次结果都是随机的。这是为什么呢?有什么解决方案?下面就来介绍。

一、synchronized介绍

1、synchronized是什么?有什么用?

synchronized是Java中用于解决并发问题的一种最常用、最简单的方法,它可以确保线程互斥的访问同步代码。

疑问:什么是并发、什么是线程互斥、什么是同步代码?

2、为什么要使用synchronized?

在并发编程中存在线程安全问题,主要原因有:
1)存在共享数据
2)多线程共同操作共享数据。
关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。

疑问:什么是线程的可见性,volatile是什么?

3、实现原理

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

疑问:临界区、共享变量、内存可见性

4、synchronized的三种应用方式

Java中每个对象都可以作为锁,这是synchronized实现同步的基础。
1)普通同步方法(实例方法),锁是当前的实例对象,进入同步代码前要获得当前实例的锁。
2)静态同步方法,锁是当前的class对象,进入同步代码前要获得当前类对象的锁
3)同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁

二、代码示例

下面对synchronized的三种应用写几个示例:

1、synchronized作用于代码块

1)多个线程访问同一个对象的synchronized代码块

【demo2】

public class SynchronizedTest implements Runnable {
    //共享资源
    public static int i = 0;

    @Override
    public void run() {
        synchronized (this){
            for (int j = 0; j < 10; j++) {
                i++;
                System.out.println(Thread.currentThread().getName() + i);
            }
        }
    }
}

public class MainActivityTest {

    @Test
    public void onCreate() throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();

        Thread t1 = new Thread(synchronizedTest);
        Thread t2 = new Thread(synchronizedTest);

        t1.start();
        t2.start();

        Thread.sleep(1000);
        System.out.println(i);
    }

}

结果:

image.png

分析:当两个并发线程(t1 和t2)同时访问同一个对象(synchronizedTest)中的synchronized代码块时,在同一时刻只能由一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行此代码块。t1和t2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。

如果我们把 synchronizedTest 的调用改一下:

public class MainActivityTest {

    @Test
    public void onCreate() throws InterruptedException {

        Thread t1 = new Thread(new SynchronizedTest() , "synchronized-1 : ");
        Thread t2 = new Thread(new SynchronizedTest(), "synchronized-2 : ");

        t1.start();
        t2.start();

        Thread.sleep(1000);
        System.out.println(i);
    }

}

结果:

image.png

分析:前面说了,一个线程执行synchronized代码块的时候,其他线程会受阻塞。但是,上面的例子显示t1和t2在同时执行。这是因为synchronized只锁定对象,每个对象只有一个锁(lock)与之关联。
在此例中,我们创建了两个SynchronizedTest对象,此时两个线程执行的是两个不同对象的synchronized代码块,这两个对象是被两把锁分别锁定的,而这两把锁是互不干扰的,并不互斥,所以两个线程可以同时执行。

2)当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。

【demo3】

class SynchronizedTest implements Runnable {
    private int count;

    public SynchronizedTest() {
        count = 0;
    }

    public void countAdd() {
        synchronized (this) {
            for (int i = 0; i < 5000; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
            }
        }
    }

    //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
    public void printCount() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " count:" + count);
        }
    }

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.equals("A")) {
            countAdd();
        } else if (threadName.equals("B")) {
            printCount();
        }
    }
}

结果:

image.png

分析:上面代码中countAdd是一个synchronized的,printCount是非synchronized的。从上面的结果中可以看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。

2、指定给某个对象加锁

【demo4】

/**
 * 银行账户类
 */
class Account {
    String name;
    float amount;

    public Account(String name, float amount) {
        this.name = name;
        this.amount = amount;
    }
    //存钱
    public void deposit(float amt) {
        amount += amt;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //取钱
    public void withdraw(float amt) {
        amount -= amt;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public float getBalance() {
        return amount;
    }
}

/**
 * 账户操作类
 */
class AccountOperator implements Runnable {
    private Account account;
    public AccountOperator(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        synchronized (account) {
            account.deposit(500);
            account.withdraw(500);
            Log.d("account " , Thread.currentThread().getName() + ":" + account.getBalance());
        }
    }
}

调用:
Account account = new Account("zhang san", 10000.0f);
        AccountOperator accountOperator = new AccountOperator(account);

        final int THREAD_NUM = 5;
        Thread threads[] = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i ++) {
            threads[i] = new Thread(accountOperator, "Thread" + i);
            threads[i].start();
        }
image.png

分析:在AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。这时,当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。
当有一个明确的对象作为锁时,就可以用类似下面这样的方式写程序。

public void method3(SomeObject obj) {
//obj 锁定的对象
    synchronized(obj) {
// todo
    }
}

当没有明确的对象作为锁时,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:**

class Test implements Runnable {
        private byte[] lock = new byte[0]; // 特殊的instance变量
        public void method() {
            synchronized(lock) {
// todo 同步代码块
            }
        }

        public void run() {

        }
}

分析:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

3、修饰一个方法(非静态)

要修饰一个方法,只需在方法前面加上synchronized即可。synchronized修饰方法和修饰代码块类似,只是作用范围不同,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。
【demo5】:
下面这两种所实现的效果是一样的:
1)synchronized修饰代码块:

@Override
    public void run() {
        synchronized (this){
            for (int j = 0; j < 10; j++) {
                i++;
                System.out.println(Thread.currentThread().getName() + i);
            }
        }
    }

2)synchronized修饰一个方法

@Override
    public synchronized void run() {
            for (int j = 0; j < 10; j++) {
                i++;
                System.out.println(Thread.currentThread().getName() + i);
            }
    }

4、synchronized修饰静态方法

【demo6】

/**
 * 同步线程
 */
class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public synchronized static void method() {
        for (int i = 0; i < 5; i ++) {
            try {
                Log.d("thread1",Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public synchronized void run() {
        method();
    }
}

调用:

SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();

结果:

image.png

分析:syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。

5、修饰一个类

class ClassName {
        public void method() {
            synchronized(ClassName.class) {
// todo
            }
        }
}

把【demo6】修改如下,效果是一样的:
【demo7】

/**
 * 同步线程
 */
class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public static void method() {
        synchronized (SyncThread.class){
            for (int i = 0; i < 5; i ++) {
                try {
                    Log.d("thread1",Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public synchronized void run() {
        method();
    }
}

分析:synchronized作用于一个类时,是给这个类加锁,对此类的所有对象用的是同一把锁。

三、总结

A. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

目前只是对synchronized有了基本的认识和了解了一些基本的使用,要深入理解,还需多多思考,在Android源码中有很多地方用到了synchronized,看源码的时候再结合今天写的笔记多做反思与总结。

然后就是很多博客都提到了一本书:《Java编程思想》,一定要找机会拜读一下。

参考:
# Java中Synchronized的用法

建议有时间看看这篇(我还没看):
https://www.jianshu.com/p/d53bf830fa09

相关文章

网友评论

      本文标题:synchronized初学笔记

      本文链接:https://www.haomeiwen.com/subject/sosluktx.html