面试题:
1.如何保证多线程下 i++ 结果正确?
2.一个线程如果出现了运行时异常会怎么样?
3.一个线程运行时发生异常会怎样?
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
(1) 阻塞式的解决方案:synchronized,Lock
(2) 非阻塞式的解决方案:原子变量
synchronized 即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
1. synchronized 同步方法
当使用synchronized关键字修饰一个方法的时候,该方法被声明为同步方法,关键字synchronized的位置处于同步方法的返回类型之前。
public class SafeDemo { // 临界区资源 private static int i = 0; // 临界区代码 public void selfIncrement(){ for(int j=0;j<5000;j++){ i++; } } public int getI(){ return i; } }
public class ThreadDemo { public static void main(String[] args) throws InterruptedException { SafeDemo safeDemo = new SafeDemo(); // 线程1和线程2同时执行临界区代码段 Thread t1 = new Thread(()->{ safeDemo.selfIncrement(); }); Thread t2 = new Thread(()->{ safeDemo.selfIncrement(); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(safeDemo.getI()); // 9906 } }
可以发现,当2个线程同时访问临界区的selfIncrement()
方法时,就会出现竞态条件的问题,即2个线程在临界区代码段的并发执行结果因为代码的执行顺序不同而导致结果无法预测,每次运行都会得到不一样的结果。因此,为了避免竞态条件的问题,我们必须保证临界区代码段操作具备排他性。这就意味着当一个线程进入临界区代码段执行时,其他线程不能进入临界区代码段执行。
现在使用synchronized关键字对临界区代码段进行保护,代码如下:
public class SafeDemo { // 临界区资源 private static int i = 0; // 临界区代码使用synchronized关键字进行保护 public synchronized void selfIncrement(){ for(int j=0;j<5000;j++){ i++; } } public int getI(){ return i; } }
经过多次运行测试用例程序,累加10000次之后,最终的结果不再有偏差,与预期的结果(10000)是相同的。
在方法声明中设置synchronized同步关键字,保证其方法的代码执行流程是排他性的。任何时间只允许一个线程进入同步方法(临界区代码段),如果其他线程需要执行同一个方法,那么只能等待和排队。
2. synchronized 方法将对象作为锁
定义线程的执行逻辑:
public class ThreadTask { // 临界区代码使用synchronized关键字进行保护 public synchronized void test() { try { System.out.println(Thread.currentThread().getName()+" begin"); Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+" end"); } catch (InterruptedException e) { e.printStackTrace(); } } }
分别创建两个线程,在两个线程的执行体中执行线程逻辑:
public class ThreadA extends Thread { ThreadTask threadTask ; public ThreadA(ThreadTask threadTask){ super(); this.threadTask = threadTask; } @Override public void run() { threadTask.test(); } }
public class ThreadB extends Thread { ThreadTask threadTask ; public ThreadB(ThreadTask threadTask){ super(); this.threadTask = threadTask; } @Override public void run() { threadTask.test(); } }
创建一个锁对象,传给两个线程:
public class Main { public static void main(String[] args) throws InterruptedException { ThreadTask threadTask = new ThreadTask(); ThreadA t1 = new ThreadA(threadTask); ThreadB t2 = new ThreadB(threadTask); t1.start(); t2.start(); } }
执行结果:
Thread-0 begin
Thread-0 end
Thread-1 begin
Thread-1 end
这里两个线程的锁对象都是threadTask,所以同一时间只有一个线程能拿到这个锁对象,执行同步代码块。另外,需要牢牢记住“共享”这两个字,只有共享资源的写访问才需要同步化,如果不是共享资源,那么就没有同步的必要。
总结:
(1) A线程先持有object对象的锁,B线程如果在这时调用object对象中的synchronized类型的方法,则需等待,也就是同步;
(2) 在方法声明处添加synchronized并不是锁方法,而是锁当前类的对象;
(3) 在Java中只有将对象作为锁,并没有锁方法这种说法;
(4) 在Java语言中,锁就是对象,对象可以映射成锁,哪个线程拿到这把锁,哪个线程就可以执行这个对象中的synchronized同步方法;
(5) 如果在X对象中使用了synchronized关键字声明非静态方法,则X对象就被当成锁;
3. 多个锁对象
创建两个线程执行逻辑ThreadTask对象,即产生了两把锁
public class Main { public static void main(String[] args) throws InterruptedException { ThreadTask threadTask1 = new ThreadTask(); ThreadTask threadTask2 = new ThreadTask(); // 两个线程分别执行两个不同的线程执行逻辑对象 ThreadA t1 = new ThreadA(threadTask1); ThreadB t2 = new ThreadB(threadTask2); t1.start(); t2.start(); } }
执行结果:
Thread-0 begin
Thread-1 begin
Thread-0 end
Thread-1 end
test()
方法使用了synchronized关键字,任何时间只允许一个线程进入同步方法,如果其他线程需要执行同一个方法,那么只能等待和排队。执行结果呈现了两个线程交叉输出的效果,说明两个线程以异步方式同时运行。
在系统中产生了两个锁,ThreadA的锁对象是threadTask1,ThreadB的锁对象是threadTas2,线程和业务对象属于一对一的关系,每个线程执行自己所属业务对象中的同步方法,不存在锁的争抢关系,所以运行结果是异步的。
synchronized方法的同步锁实质上使用了this对象锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象作为锁(哪个对象调用了带有synchronized关键字的方法,哪个对象就是锁),其他线程只能等待,前提是多个线程访问的是同一个对象。
4. 如果同步方法内的线程抛出异常会发生什么?
public class SafeDemo { public synchronized void selfIncrement(){ if(Thread.currentThread().getName().equals("t1")){ System.out.println("t1 线程正在运行"); int a=1; // 死循环,只要t1线程没有执行完这个方法,就不会释放锁 while (a==1){ } }else{ System.out.println("t2 线程正在运行"); } } }
public class SafeDemo { public synchronized void selfIncrement(){ if(Thread.currentThread().getName().equals("t1")){ System.out.println("t1 线程正在运行"); int a=1; while (a==1){ Integer.parseInt("a"); } }else{ System.out.println("t2 线程正在运行"); } } }
执行结果:t2线程得不到执行
t1 线程正在运行
此时,如果我们在同步方法中制造一个异常:
public class SafeDemo { public synchronized void selfIncrement(){ if(Thread.currentThread().getName().equals("t1")){ System.out.println("t1 线程正在运行"); int a=1; while (a==1){ Integer.parseInt("a"); } }else{ System.out.println("t2 线程正在运行"); } } }
线程t1出现异常并释放锁,线程t2进入方法正常输出,说明出现异常时,锁被自动释放了。
5. 静态的同步方法
在Java世界里一切皆对象。Java有两种对象:Object实例对象和Class对象。每个类运行时的类型信息用Class对象表示,它包含与类名称、继承关系、字段、方法有关的信息。JVM将一个类加载入自己的方法区内存时,会为其创建一个Class对象,对于一个类来说其Class对象是唯一的。Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机调用类加载器中的defineClass方法自动构造的,因此不能显式地声明一个Class对象。
普通的synchronized实例方法,其同步锁是当前对象this的监视锁。如果某个synchronized方法是static(静态)方法,而不是普通的对象实例方法,其同步锁又是什么呢?
public class StaticSafe { // 临界资源 private static int count = 0; // 使用synchronized关键字修饰static方法 public static synchronized void test(){ count++; } }
静态方法属于Class实例而不是单个Object实例,在静态方法内部是不可以访问Object实例的this引用的。所以,修饰static方法的synchronized关键字就没有办法获得Object实例的this对象的监视锁。
实际上,使用synchronized关键字修饰static方法时,synchronized的同步锁并不是普通Object对象的监视锁,而是类所对应的Class对象的监视锁。
为了以示区分,这里将Object对象的监视锁叫作对象锁,将Class对象的监视锁叫作类锁。当synchronized关键字修饰static方法时,同步锁为类锁;当synchronized关键字修饰普通的成员方法时,同步锁为对象锁。由于类的对象实例可以有很多,但是每个类只有一个Class实例,因此使用类锁作为synchronized的同步锁时会造成同一个JVM内的所有线程只能互斥地进入临界区段。
public class StaticSafe { // 临界资源 private static int count = 0; // 对JVM内的所有线程同步 public static synchronized void test(){ count++; } } z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z
所以,使用synchronized关键字修饰static方法是非常粗粒度的同步机制。
通过synchronized关键字所抢占的同步锁什么时候释放呢?一种场景是synchronized块(代码块或者方法)正确执行完毕,监视锁自动释放;另一种场景是程序出现异常,非正常退出synchronized块,监视锁也会自动释放。所以,使用synchronized块时不必担心监视锁的释放问题。
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!