一、为什么要使用线程同步
1、什么是同步
同步就是协同步调,按预定的先后次序进行运行。如:你用完,其它人才能用。“同”字从字面上容易理解为一起
其实不是,“同”字应是指协同、协助、互相配合。
当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。
2、如果不用同步产生的问题
当多个线程同时读写同一份共享资源的时候,有可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到。比如下面不使用同步会产生的问题
-
举个栗子
public class Bank { /** * 账户余额 */ private int count = 0; /** * 存钱 * * @param money */ public void addMoney(int money) { count += money; System.out.println(System.currentTimeMillis() + "存进:" + money); } /** * 取钱 * * @param money */ public void delMoney(int money) { if (count - money < 0) { System.out.println("余额不足"); return; } count -= money; System.out.println(System.currentTimeMillis() + "取出:" + money); } /** * 查询 */ public void lookMoney() { System.out.println("账户余额:" + count); } }
// 两个线程 不停的存钱 取钱 public class SyncThreadBankExample { public static void main(String[] args){ final Bank bank=new Bank(); // 存钱线程 Thread tadd=new Thread(() -> { while(true){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 存100块到账户 bank.addMoney(100); // 查看余额 bank.lookMoney(); System.out.println("\n"); } }); // 取钱的线程 Thread tsub = new Thread(() -> { while(true){ bank.delMoney(100); bank.lookMoney(); System.out.println("\n"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }); tsub.start(); tadd.start(); } }
1564304836893取出:100 账户余额:1200 1564304836893存进:100 账户余额:1200
3、什么时候使用同步
-
多个线程执行的时候需要同步,如果是单线程则不需要同步。
-
多个线程在执行的过程中是不是使用同一把锁。
4、常见的同步方案
- synchronized(同步)
- ReentrantLock (可重入锁)
- Atomic( 原子类,效率较高,可用于优化 )
- ThreadLocal (线程本地变量)
二、同步(synchronized)
1、说明
synchronized 是 Java 中的关键字,是利用锁的机制来实现同步的。synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,这是synchronized实现同步的基础。
每个进程中访问临界资源的那段代码称为临界区(临界资源是同一时刻一次仅允许一个进程使用的共享资源)
2、锁机制有如下两种特性:
-
互斥性:
即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。
-
可见性
必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。
3、类锁和对象锁
-
类锁
在代码中的方法上加了
static和synchronized
的锁,或者synchronized(xxx.class)
-
对象锁
在代码中的方法上加了
synchronized
的锁,或者synchronized(this)
的代码段 -
注意
- 类锁和对象锁不会产生竞争,二者的加锁方法不会相互影响。
- 一个实例对象一把锁,多个实例对象多把锁.多线程解决高并发只能通过一个加锁实例实现
4、根据修饰对象用法分类
- 修饰代码块
- synchronized(this|object) {}
- synchronized(类.class) {}
- 修饰方法
- 修饰非静态方法
- 修饰静态方法
5、修饰代码块锁-对象
-
说明
在代码中的方法上加了
synchronized
的锁,或者synchronized(this)
的代码段如果一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
1、在调用的方法中使用
-
举个栗子
public class SyncThreadExample implements Runnable { private int count; public SyncThreadExamp1() { count = 0; } @Override public void run() { // 锁定对象 synchronized (this) { for (int i = 0; i < 10; i++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public class TestSyncThreadExample { public static void main(String[] args) { SyncThreadExample syncThread = new SyncThreadExample(); Thread t1 = new Thread(syncThread, "同步线程1"); Thread t2 = new Thread(syncThread, "同步线程2"); t1.start(); t2.start(); } /** * 同步线程1:6 * 同步线程1:7 * 同步线程1:8 * 同步线程1:9 * 同步线程2:10 * 同步线程2:11 * 同步线程2:12 * 同步线程2:13 * 同步线程2:14 * 同步线程2:15 */ }
-
说明
-
当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。
-
Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
-
-
修改代码
Thread t1 = new Thread(new SyncThreadExample(), "同步线程1"); Thread t2 = new Thread(new SyncThreadExample(), "同步线程2"); t1.start(); t2.start(); /** * 同步线程1:0 * 同步线程2:0 * 同步线程1:1 * 同步线程2:1 */
-
分析
- 我们知道synchronized锁定的是对象,这时会有两把锁分别锁定Thread1对象和Thread2对象,
- 而这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。
2、多个线程访问synchronized和非synchronized代码块
-
举个栗子
public class Counter implements Runnable { private int count = 0; public static final int MAX_NUM = 10; public static final String CONS_ADD = "add"; /** * 同步方法 不允许多个线程同时访问 */ private void add() { synchronized (this) { for (int i = 0; i < MAX_NUM; i++) { try { count++; System.out.println(Thread.currentThread().getName() + "====>" + count); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * 非同步方法 允许多个线程同时访问 */ private void find() { for (int i = 0; i < MAX_NUM; i++) { try { System.out.println(Thread.currentThread().getName() + " count:" + count); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } @Override public void run() { String threadName = Thread.currentThread().getName(); if (CONS_ADD.equals(threadName)) { add(); } else { find(); } } }
public class TestSyncThreadCounter { public static void main(String[] args) { Counter counter = new Counter(); Thread thread1 = new Thread(counter, "add"); Thread thread2 = new Thread(counter, "find"); thread1.start(); thread2.start(); } /** * add====>3 * find count:3 * add====>4 * find count:4 * add====>5 * find count:5 * find count:6 * add====>6 * add====>7 * find count:7 */ }
-
说明
- 上面代码中add是一个synchronized的,find是非synchronized的。
- 从上面的结果中可以看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。
3、指定要给某个对象加锁
-
举个栗子
public class Account { private String cardNo; private float balance; public Account() { } public Account(String cardNo, float balance) { this.cardNo = cardNo; this.balance = balance; } public void put(float balance) { this.balance += balance; try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 取钱 * * @param balance */ public void del(float balance) { this.balance -= balance; try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 查询余额 */ public float find() { return balance; } }
public class TestAccountOperator { public static void main(String[] args) { Account account = new Account(); AccountOperator operator = new AccountOperator(account); for (int i = 0; i < 10; i++) { new Thread(operator).start(); } } }
-
说明
在AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。这时,当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束
6、修饰代码块锁-类
-
说明
对一个类加锁时,对该类的所有对象都起作用。跟静态方法是相同,因为静态方法本身就是属于类的
-
举个栗子
public class SyncThreadExample1 implements Runnable { private int count; public SyncThreadExample1() { count = 0; } @Override public void run() { synchronized (SyncThreadExample1.class) { for (int i = 0; i < 10; i++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
public class TestSyncThreadExample1 { public static void main(String[] args) { Thread t1 = new Thread(new SyncThreadExample1(), "同步线程1"); Thread t2 = new Thread(new SyncThreadExample1(), "同步线程2"); t1.start(); t2.start(); /** * 同步线程1:6 * 同步线程1:7 * 同步线程1:8 * 同步线程1:9 * 同步线程2:0 * 同步线程2:1 * 同步线程2:2 * 同步线程2:3 * 同步线程2:4 * 同步线程2:5 */ } }
7、修饰一个方法
-
说明
在执行该对象的这个同步方法(注意:是该对象)就会产生互斥
-
格式
public synchronized void method(){ }
7、修饰一个静态的方法
-
说明
Synchronized也可修饰一个静态方法
-
举个栗子
public synchronized static void method() { }