一、银行取款引出的问题
模拟银行取钱的例子:
public class ThreadDemo06 { public static void main(String[] args) { Bank bank = new Bank(); Runnable runnable = new MoneyThread(bank); Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); } } class Bank { private int money = 1000; public int get(int number) { if (number < 0) { return -1; } else if (number > money) { return -2; } else if (money < 0) { return -3; } else { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } money -= number; System.err.println("还剩:" + money); return number; } } } class MoneyThread implements Runnable { private Bank bank; public MoneyThread(Bank bank) { this.bank = bank; } @Override public void run() { bank.get(800); } }
运行可能的结果:
还剩:200
还剩:-600
造成此类问题的根本原因在于,多个线程在操作共享的数据或者操作共享数据的线程代码有多行,当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程的安全问题的产生。
二、问题的解决方案
在线程使用一个资源时为其加锁即可。访问资源的第一个线程为其加上锁以后,其他线程就不能再使用那个资源,除非被解锁。同一时间段内只能有一个线程进行,其他线程要等待此线程完成之后才可以继续执行。
将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码时其他线程是不可以参与运算的,必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算;
保证取钱和修改余额同时完成:
1)使用同步代码块,synchronized(obj){},还需要一个同步监听对象;
2)使用同步方法,使用synchronized去修饰需要同步的方法;
方法一:同步方法
在Java中通过synchronized关键字来完成对对象的加锁。
上述代码加锁的解决方案如下:
class Bank { private int money = 1000; public synchronized int get(int number) { if (number < 0) { return -1; } else if (number > money) { return -2; } else if (money < 0) { return -3; } else { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } money -= number; System.err.println("还剩:" + money); return number; } } }
方法二:同步代码块
synchronized块写法:
synchronized(object){
} //表示线程在执行的时候会对object对象上锁
这里的object可以是任意对象,但必须保证多个线程持有的这个object是同一个对象;
synchronized方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行该synchronized方法;synchronized块则是一种细粒度的并发控制,只有将块中的代码同步,位于方法内、synchronized块之外的代码是可以被多个线程同时访问到的。
三、线程同步的关键知识点
1)Java中的每个对象都有一个锁(lock)或者叫做监视器(monitor),当访问某个对象的synchronized方法时,表示将该对象上锁,此时其他任何线程都无法再去访问该对象的synchronized方法了,直到之前的那个线程执行方法完毕后(或者是抛出了异常),那么将该对象的锁释放掉,其他线程才可能再去访问该对象的其他synchronized方法;
public class ThreadDemo07 { public static void main(String[] args) { Example example = new Example(); Runnable runnable = new TheThread(example); //同一对象 Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); } } class Example { public synchronized void execute() { for (int i = 0; i < 10; i++) { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("execute :" + i); } } } class TheThread implements Runnable { private Example example; public TheThread(Example example) { this.example = example; } public void run() { example.execute(); } }
上述代码的执行:由于是对同一对象产生的线程,当两个不同线程进行访问的时候,谁先进入synchronized方法就将该example对象上锁了,其他线程就没有办法再进入该对象的任何同步方法了,所以只有当一个线程执行完毕或者抛出异常后第二个线程才能进行访问。
public class ThreadDemo08 { public static void main(String[] args) { Runnable runnable = new TheThread(new Example()); Runnable runnable2 = new TheThread(new Example()); Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable2); thread1.start(); thread2.start(); } } class Example { public synchronized void execute() { for (int i = 0; i < 10; i++) { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("execute :" + i); } } } class TheThread implements Runnable { private Example example; public TheThread(Example example) { this.example = example; } public void run() { example.execute(); } }
上述代码的执行:由于是两个线程对两个不同对象进行访问操作。那么这2个线程就没有任何关联,各自访问各自的对象,互不干扰。
public class ThreadDemo09 { public static void main(String[] args) { Example example = new Example(); Runnable runnable = new TheThread(example);//同一对象 Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); } } class Example { public synchronized void execute() { for (int i = 0; i < 10; i++) { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("execute :" + i); } } public synchronized void execute2() { for (int i = 0; i < 10; i++) { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("execute2 :" + i); } } } class TheThread implements Runnable { private Example example; public TheThread(Example example) { this.example = example; } public void run() { example.execute(); } } class TheThread2 implements Runnable { private Example example; public TheThread2(Example example) { this.example = example; } public void run() { example.execute2(); } }
上述代码的执行结果:是由同一对象生成的两个不同的线程,当两个不同的线程访问同一对象不同的synchronized方法时,谁先进入第一个synchronized方法,那么该线程就将该对象上锁了,其他线程是没有办法再对该对象的任何synchronized方法进行访问。
public class ThreadDemo10 { public static void main(String[] args) { Example example = new Example(); Runnable runnable = new TheThread(example); Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); } } class Example { public synchronized static void execute() { for (int i = 0; i < 10; i++) { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("execute :" + i); } } public synchronized static void execute2() { for (int i = 0; i < 10; i++) { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("execute2 :" + i); } } }
上述代码的执行结果:由于静态方法是属于类级别的,当一个方法锁住后,只有等第一个线程执行完毕以后第二个线程才能进入。
2)synchronized方法使用了static关键字进行修饰,表示将该对象的Class对象加锁
public class ThreadDemo11 { public static void main(String[] args) { Runnable runnable = new TheThread(new Example()); Runnable runnable2 = new TheThread(new Example()); Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable2); thread1.start(); thread2.start(); } } class Example { public synchronized static void execute() { for (int i = 0; i < 10; i++) { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("execute :" + i); } } public synchronized static void execute2() { for (int i = 0; i < 10; i++) { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("execute2 :" + i); } } }
上述代码的执行结果:虽然是针对两个不同对象生成的不同的线程,但是由于synchronized方法使用了static关键字进行修饰,表示将该对象的Class对象加锁。所以只有等一个线程执行完毕后,其他线程才能进入访问。
3) 如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。
4)如果某个synchronized方法是static的,那么当线程访问该方法时,它的锁并不是synchronized方法所在的对象,而是synchronized方法所在的对象所对应的Class对象,因为Java中无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的两个static, synchronized方法时,他们的执行顺序也是有顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始执行。
public class ThreadDemo02 { public static void main(String[] args) { C c = new C(); Thread t1 = new T1(c); Thread t2 = new T2(c); t1.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } } class C { public synchronized static void hello() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("hello"); } public synchronized void world() { System.out.println("world"); } } class T1 extends Thread { private C c; public T1(C c) { this.c = c; } @Override public void run() { c.hello(); } } class T2 extends Thread { private C c; public T2(C c) { this.c = c; } @Override public void run() { c.world(); } }
执行结果:先执行world,然后才输出hello,原因是static是给当前对象的Class对象上锁,而没有static的是给当前对象上锁,两把锁锁的对象不同,所以并没有影响。
四、线程同步总结
synchronized修饰方法
1)非静态方法:默认的同步监听器对象是this;
2))静态方法:默认的同步监听器对象是该方法所在类的Class对象。
若线程是实现方式
1)同步代码块:同步监听对象可以选this、这个方法所在类的Class对象、任一不变对象;
2)同步方法:此时可以使用synchronized直接修饰run方法,因为同步监听器是this。
若线程是继承方式
1)同步代码块:同步监听器可以选用该方法所在类的Class对象、任一不变对象;
2)同步方法:此时不能使用synchronized直接修饰run方法;
3)总结:只要是继承方式,不论是同步代码块还是同步方法均不能使用this。
同步的利弊
1)好处:解决了线程的安全问题;
2)弊端:相对降低了效率,因为同步外的线程的都会判断同步锁;
3)前提:同步中必须有多个线程并使用同一个锁。