本篇主要介绍 Java 多线程中的同步, 也就是如何在 Java 语言中写出线程安全的程序, 如何在 Java 语言中解决非线程安全的相关问题, 没错就是使用 synchronized.
一, 如何解决线程安全问题?
一般来说, 是如何解决线程安全问题的呢?
基本上所有的并发模式在解决线程安全问题时, 都采用 "序列化访问临界资源" 的方案, 即在同一时刻, 只能有一个线程访问临界资源, 也称作同步互斥访问.
通常来说, 是在访问临界资源的代码前面加上一个锁, 当访问完临界资源后释放锁, 让其他线程继续访问.
在 Java 中, 提供了两种方式来实现同步互斥访问: synchronized 和 Lock. 本文主要讲述 synchronized 的使用方法, Lock 的使用方法后面再说.
二, synchronized 关键字
1. 简介
synchronized, 我们谓之锁, 主要用来给方法, 代码块加锁. 当某个方法或者代码块使用 synchronized 时, 那么在同一时刻至多仅有有一个线程在执行该段代码. 当有多个线程访问同一对象的加锁方法 / 代码块时, 同一时间只有一个线程在执行, 其余线程必须要等待当前线程执行完之后才能执行该代码段. 但是, 其余线程是可以访问该对象中的非加锁代码块的.
synchronized 主要包括两种方法: synchronized 方法, synchronized 块.
2.synchronized 方法
通过在方法声明中加入 synchronized 关键字来声明 synchronized 方法. 如:
public synchronized void getResult();
synchronized 方法控制对类成员变量的访问. 它是如何来避免类成员变量的访问控制呢? 我们知道方法使用了 synchronized 关键字表明该方法已加锁, 在任一线程在访问改方法时都必须要判断该方法是否有其他线程在 "独占". 每个类实例对应一个把锁, 每个 synchronized 方法都必须调用该方法的类实例的锁方能执行, 否则所属线程阻塞, 方法一旦执行, 就独占该锁, 直到从该方法返回时才将锁释放, 被阻塞的线程方能获得该锁.
其实 synchronized 方法是存在缺陷的, 如果我们将一个很大的方法声明为 synchronized 将会大大影响效率的. 如果多个线程在访问一个 synchronized 方法, 那么同一时刻只有一个线程在执行该方法, 而其他线程都必须等待, 但是如果该方法没有使用 synchronized, 则所有线程可以在同一时刻执行它, 减少了执行的总时间. 所以如果我们知道一个方法不会被多个线程执行到或者说不存在资源共享的问题, 则不需要使用 synchronized 关键字. 但是如果一定要使用 synchronized 关键字, 那么我们可以 synchronized 代码块来替换 synchronized 方法.
3.synchronized 块
synchronized 代码块所起到的作用和 synchronized 方法一样, 只不过它使临界区变的尽可能短了, 换句话说: 它只把需要的共享数据保护起来, 其余的长代码块留出此操作. 语法如下:
- synchronized(object) {
- // 允许访问控制的代码
- }
如果我们需要以这种方式来使用 synchronized 关键字, 那么必须要通过一个对象引用来作为参数, 通常这个参数我们常使用为 this.,
- synchronized (this) {
- // 允许访问控制的代码
- }
4.synchronized 的使用
synchronized 代码块, 被修饰的代码成为同步语句块, 其作用的范围是调用这个代码块的对象, 我们在用 synchronized 关键字的时候, 能缩小代码段的范围就尽量缩小, 能在代码段上加同步就不要再整个方法上加同步. 这叫减小锁的粒度, 使代码更大程度的并发.
synchronized 方法, 被修饰的方法成为同步方法, 其作用范围是整个方法, 作用对象是调用这个方法的对象.
synchronized 静态方法, 修饰一个 static 静态方法, 其作用范围是整个静态方法, 作用对象是这个类的所有对象.
synchronized 类, 其作用范围是 Synchronized 后面括号括起来的部分 synchronized(className.class), 作用的对象是这个类的所有对象.
synchronized(),()中是锁住的对象, synchronized(this)锁住的只是对象本身, 同一个类的不同对象调用的 synchronized 方法并不会被锁住, 而 synchronized(className.class)实现了全局锁的功能, 所有这个类的对象调用这个方法都受到锁的影响, 此外 () 中还可以添加一个具体的对象, 实现给具体对象加锁.
5.synchronized 注意事项
当两个并发线程访问同一个对象中的 synchronized 代码块时, 在同一时刻只能有一个线程得到执行, 另一个线程受阻塞, 必须等待当前线程执行完这个代码块以后才能执行该代码块. 两个线程间是互斥的, 因为在执行 synchronized 代码块时会锁定当前的对象, 只有执行完该代码块才能释放该对象锁, 下一个线程才能执行并锁定该对象.
当一个线程访问 object 的一个 synchronized(this)同步代码块时, 另一个线程仍然可以访问该 object 中的非 synchronized(this)同步代码块.(两个线程使用的是同一个对象)
当一个线程访问 object 的一个 synchronized(this)同步代码块时, 其他线程对 object 中所有其它 synchronized(this)同步代码块的访问将被阻塞(同上, 两个线程使用的是同一个对象).
下面通过代码来实现:
1)当两个并发线程访问同一个对象 object 中的这个 synchronized(this)同步代码块时, 一个时间内只能有一个线程得到执行. 另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块.
- public class Thread1 implements Runnable {
- public void run() {
- synchronized(this) {
- for (int i = 0; i <5; i++) {
- System.out.println(Thread.currentThread().getName() + "synchronized loop" + i);
- }
- }
- }
- public static void main(String[] args) {
- Thread1 t1 = new Thread1();
- Thread ta = new Thread(t1, "A");
- Thread tb = new Thread(t1, "B");
- ta.start();
- tb.start();
- }
- }
输出结果:
- A synchronized loop 0
- A synchronized loop 1
- A synchronized loop 2
- A synchronized loop 3
- A synchronized loop 4
- B synchronized loop 0
- B synchronized loop 1
- B synchronized loop 2
- B synchronized loop 3
- B synchronized loop 4
2)然而, 当一个线程访问 object 的一个 synchronized(this)同步代码块时, 另一个线程仍然可以访问该 object 中的非 synchronized(this)同步代码块.
- public class Thread2 {
- public void m4t1() {
- synchronized(this) {
- int i = 5;
- while( i--> 0) {
- System.out.println(Thread.currentThread().getName() + ":" + i);
- try {
- Thread.sleep(500);
- } catch (InterruptedException ie) {
- }
- }
- }
- }
- public void m4t2() {
- int i = 5;
- while( i--> 0) {
- System.out.println(Thread.currentThread().getName() + ":" + i);
- try {
- Thread.sleep(500);
- } catch (InterruptedException ie) {
- }
- }
- }
- public static void main(String[] args) {
- final Thread2 myt2 = new Thread2();
- Thread t1 = new Thread( new Runnable() { public void run() { myt2.m4t1(); } }, "t1" );
- Thread t2 = new Thread( new Runnable() { public void run() { myt2.m4t2(); } }, "t2" );
- t1.start();
- t2.start();
- }
- }
输出结果:
- t1 : 4
- t2 : 4
- t1 : 3
- t2 : 3
- t1 : 2
- t2 : 2
- t1 : 1
- t2 : 1
- t1 : 0
- t2 : 0
3)尤其关键的是, 当一个线程访问 object 的一个 synchronized(this)同步代码块时, 其他线程对 object 中所有其它 synchronized(this)同步代码块的访问将被阻塞.
- // 修改 Thread2.m4t2()方法:
- public void m4t2() {
- synchronized(this) {
- int i = 5;
- while( i--> 0) {
- System.out.println(Thread.currentThread().getName() + ":" + i);
- try {
- Thread.sleep(500);
- } catch (InterruptedException ie) {
- }
- }
- }
- }
输出结果:
- t1 : 4
- t1 : 3
- t1 : 2
- t1 : 1
- t1 : 0
- t2 : 4
- t2 : 3
- t2 : 2
- t2 : 1
- t2 : 0
4)第三个例子同样适用其它同步代码块. 也就是说, 当一个线程访问 object 的一个 synchronized(this)同步代码块时, 它就获得了这个 object 的对象锁. 结果, 其它线程对该 object 对象所有同步代码部分的访问都被暂时阻塞.
- // 修改 Thread2.m4t2()方法如下:
- public synchronized void m4t2() {
- int i = 5;
- while( i--> 0) {
- System.out.println(Thread.currentThread().getName() + ":" + i);
- try {
- Thread.sleep(500);
- } catch (InterruptedException ie) {
- }
- }
- }
输出结果:
- t1 : 4
- t1 : 3
- t1 : 2
- t1 : 1
- t1 : 0
- t2 : 4
- t2 : 3
- t2 : 2
- t2 : 1
- t2 : 0
5)每个类也会有一个锁, 它可以用来控制对 static 数据成员的并发访问.
并且如果一个线程执行一个对象的非 static synchronized 方法, 另外一个线程需要执行这个对象所属类的 static synchronized 方法, 此时不会发生互斥现象, 因为访问 static synchronized 方法占用的是类锁, 而访问非 static synchronized 方法占用的是对象锁, 所以不存在互斥现象.
代码如下:
- public class Test {
- public static void main(String[] args) {
- final InsertData insertData = new InsertData();
- new Thread(){
- @Override
- public void run() {
- insertData.insert();
- }
- }.start();
- new Thread(){
- @Override
- public void run() {
- insertData.insert1();
- }
- }.start();
- }
- }
- class InsertData {
- public synchronized void insert(){
- System.out.println("执行 insert");
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("执行 insert 完毕");
- }
- public synchronized static void insert1() {
- System.out.println("执行 insert1");
- System.out.println("执行 insert1 完毕");
- }
- }
输出结果:
执行 insert
执行 insert1
执行 insert1 完毕
执行 insert 完毕
第一个线程里面执行的是 insert 方法, 不会导致第二个线程执行 insert1 方法发生阻塞现象.
三, 面试题
当一个线程进入一个对象的 synchronized 方法 A 之后, 其它线程是否可进入此对象的 synchronized 方法 B?
答: 不能. 其它线程只能访问该对象的非同步方法, 同步方法则不能进入. 因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁, 如果已经进入 A 方法说明对象锁已经被取走, 那么试图进入 B 方法的线程就只能在等锁池 (注意不是等待池哦) 中等待对象的锁.
synchronized 关键字的用法?
答: synchronized 关键字可以将对象或者方法标记为同步, 以实现对对象和方法的互斥访问, 可以用 synchronized(对象) { ... }定义同步代码块, 或者在声明方法时将 synchronized 作为方法的修饰符.
简述 synchronized 和 java.util.concurrent.locks.Lock 的异同?
答: Lock 是 Java 5 以后引入的新的 API, 和关键字 synchronized 相比主要相同点: Lock 能完成 synchronized 所实现的所有功能; 主要不同点: Lock 有比 synchronized 更精确的线程语义和更好的性能, 而且不强制性的要求一定要获得锁. synchronized 会自动释放锁, 而 Lock 一定要求程序员手工释放, 并且最好在 finally 块中释放(这是释放外部资源的最好的地方)
以上就是 synchronized 的概念和基本使用用法; 但如想要更加深入分析 synchronized 的实现原理, 则需要继续攻关, 本文仅仅是入门级, 希望对你有帮助.
来源: https://www.cnblogs.com/pony1223/p/9221348.html