我们首先来看一个例子:
【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是:


每次都不一样。
在此例中,我们开启了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);
}
}
结果:

分析:当两个并发线程(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);
}
}
结果:

分析:前面说了,一个线程执行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();
}
}
}
结果:

分析:上面代码中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();
}

分析:在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();
结果:

分析: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编程思想》,一定要找机会拜读一下。
建议有时间看看这篇(我还没看):
https://www.jianshu.com/p/d53bf830fa09
网友评论