Java synchronized实现线程同步的2种方法(附带实例)
在多线程的程序中,有多个线程并发运行,这多个并发执行的线程往往不是孤立的,它们之间可能会共享资源,也可能要相互合作完成某一项任务,如何使多个并发执行的线程在执行过程中不产生冲突,是多线程编程必须解决的问题。否则,可能导致程序运行的结果不正确,甚至造成死锁问题。
下面讲解一个共享数据对象的例子。
【实例】多线程并发可能引发的问题。在主线程中通过同一个 Runnable 对象创建 10 个线程对象,这 10 个线程共享 Runnable 对象的成员变量 num,在线程中通过循环实现对成员变量 num 加 1000 的操作,10 个子线程运行过之后,显示相加的结果。
为了解决这一类问题,必须要引入同步机制,那么什么是同步,如何实现在多线程访问同一资源的时候保持同步?Java 使用“锁”机制来实现线程的同步。锁机制要求每个线程在进入共享代码之前都要取得锁,否则不能进入,在退出共享代码之前释放该锁,这样就能防止几个或多个线程竞争共享代码的情况,从而解决线程不同步的问题。
Java 的同步机制可以通过对关键代码段使用 synchronized 关键字修饰来实现针对该代码段的同步操作。实现同步的方式有两种,一种是利用同步代码块来实现同步,另一种是利用同步方法来实现同步。下面分别介绍这两种方法。
用 synchonized 声明的语句块称为同步代码块,同步代码块的语法格式如下:
【实例】构建一个信用卡账户,起初信用额为 10000,然后模拟透支、存款等多个操作。显然银行账户 User 对象是个竞争资源,应该把修改账户余额的语句放在同步代码块中,并将账户的余额设为私有变量,禁止直接访问。
通过在方法声明中加入 synchronized 关键字来声明 synchronized 方法,示例代码如下:
【实例】在主线程中通过同一个 Runnable 对象创建两个线程对象,Runnable 对象中有一个同步方法实现输出线程信息,一个线程输出完之后,另一个线程才能开始输出信息。在主线程中启动这两个线程,实现对同步方法的调用。
多线程引发的问题
有时候在多线程的程序设计中需要实现多个线程共享同一段代码,从而实现共享同一个私有成员或类的静态成员的目的。这时,由于线程和线程之间互相竞争 CPU 资源,使得线程无序地访问这些共享资源,最终可能导致得不到正确的结果。这些问题通常称为线程安全问题。下面讲解一个共享数据对象的例子。
【实例】多线程并发可能引发的问题。在主线程中通过同一个 Runnable 对象创建 10 个线程对象,这 10 个线程共享 Runnable 对象的成员变量 num,在线程中通过循环实现对成员变量 num 加 1000 的操作,10 个子线程运行过之后,显示相加的结果。
public class ThreadUnsafe { public static void main(String argv[]) { ShareData shareData = new ShareData(); // 实例化 shareData 对象 for (int i = 0; i < 10; i++) { new Thread(shareData).start(); // 通过 shareData 对象创建线程并启动 } } } class ShareData implements Runnable { public int num = 0; // 记数变量 private void add() { int temp; // 临时变量 // 循环体让变量 num 执行加 1 操作,使用 temp 是为了增加线程切换的概率 for (int i = 0; i < 1000; i++) { temp = num; temp++; num = temp; } // 输出线程信息和 num 的当前值 System.out.println(Thread.currentThread().getName() + "~" + num); } public void run() { add(); // 调用 add() 方法 } }程序运行结果为:
Thread-0-2000
Thread-9-9930
Thread-8-9930
Thread-7-8000
Thread-6-7000
Thread-5-6000
Thread-4-5000
Thread-3-4000
Thread-2-3000
Thread-1-2000
为了解决这一类问题,必须要引入同步机制,那么什么是同步,如何实现在多线程访问同一资源的时候保持同步?Java 使用“锁”机制来实现线程的同步。锁机制要求每个线程在进入共享代码之前都要取得锁,否则不能进入,在退出共享代码之前释放该锁,这样就能防止几个或多个线程竞争共享代码的情况,从而解决线程不同步的问题。
Java 的同步机制可以通过对关键代码段使用 synchronized 关键字修饰来实现针对该代码段的同步操作。实现同步的方式有两种,一种是利用同步代码块来实现同步,另一种是利用同步方法来实现同步。下面分别介绍这两种方法。
同步代码块
JAVA 虚拟机为每个对象配备一把锁和一个等候集,这个对象可以是实例对象,也可以是类对象。对实例对象进行加锁,可以保证与这个实例对象关联的线程可以互斥地使用对象的锁;对类对象进行加锁,可以保证与这个类相关联的线程可以互斥地使用类对象的锁。用 synchonized 声明的语句块称为同步代码块,同步代码块的语法格式如下:
synchronized(synObject) { // 关键代码 }其中的关键代码必须获得对象 synObject 的锁才能执行。当一个线程欲进入该对象的关键代码时,JVM 将检查该对象的锁是否被其他线程获得,如果没有,则 JVM 把该对象的锁交给当前请求锁的线程,该线程获得锁后就可以进入关键代码区域。
【实例】构建一个信用卡账户,起初信用额为 10000,然后模拟透支、存款等多个操作。显然银行账户 User 对象是个竞争资源,应该把修改账户余额的语句放在同步代码块中,并将账户的余额设为私有变量,禁止直接访问。
public class CreditCard { public static void main(String[] args) { // 创建一个用户对象 User u = new User("张三", 10000); // 创建6线程对象 UserThread t1 = new UserThread("线程A", u, 200); UserThread t2 = new UserThread("线程B", u, -600); UserThread t3 = new UserThread("线程C", u, -800); UserThread t4 = new UserThread("线程D", u, -300); UserThread t5 = new UserThread("线程E", u, 1000); UserThread t6 = new UserThread("线程F", u, 200); // 依次启动线程 t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); t6.start(); } } class UserThread extends Thread { private User u; // 创建一个 User 对象 private int y = 0; // 构造方法,初始化成员变量 UserThread(String name, User u, int y) { super(name); // 调用父类的构造方法,设置线程名 this.u = u; this.y = y; } public void run() { u.oper(y); // 调用 User 对象的 oper() 方法操作共享数据 } } class User { private String code; // 用户卡号 private int cash; // 用户卡上的余额 User(String code, int cash) { this.code = code; this.cash = cash; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } // 存取款操作方法 public void oper(int x) { try { Thread.sleep(10); // 把修改共享数据的语句放在同步代码块中 synchronized (this) { this.cash += x; System.out.println(Thread.currentThread().getName() + " 运行结束,增加 " + x + ",当前用户账户余额为:" + cash); } Thread.sleep(10); // 线程休眠10ms } catch (InterruptedException e) { e.printStackTrace(); } } public String toString() { return "User{" + "code='" + code + '\'' + ", cash=" + cash + '}'; } }程序运行结果为:
线程A运行结束,增加“200”,当前用户账户余额为:10200
线程F运行结束,增加“200”,当前用户账户余额为:10400
线程C运行结束,增加“-800”,当前用户账户余额为:9600
线程D运行结束,增加“-300”,当前用户账户余额为:9300
线程E运行结束,增加“1000”,当前用户账户余额为:10300
线程B运行结束,增加“-600”,当前用户账户余额为:9700
同步方法
同步方法和同步代码块的功能是一样的,都是利用互斥锁实现关键代码的同步访问。只不过在这里通常关键代码就是一个方法的方法体,此时只需要调用 synchronized 关键字修饰该方法即可。一旦被 synchronized 关键字修饰的方法已被一个线程调用,那么所有其他试图调用同一实例中的该方法的线程都必须等待,直到该方法被调用结束,释放锁给下一个等待的线程。通过在方法声明中加入 synchronized 关键字来声明 synchronized 方法,示例代码如下:
public synchronized void accessVal(int newVal);
【实例】在主线程中通过同一个 Runnable 对象创建两个线程对象,Runnable 对象中有一个同步方法实现输出线程信息,一个线程输出完之后,另一个线程才能开始输出信息。在主线程中启动这两个线程,实现对同步方法的调用。
public class PrintThread { private String name; public static void main(String[] args) { MethodSync ms = new MethodSync(); // 实例化 MethodSync 对象 Thread t1 = new Thread(ms, "线程 A"); // 通过 MethodSync 对象创建线程 Thread t2 = new Thread(ms, "线程 B"); // 通过 MethodSync 对象创建线程 t1.start(); // 启动线程 t2.start(); // 启动线程 } } class MethodSync implements Runnable { // 同步方法 public synchronized void show() { System.out.println(Thread.currentThread().getName() + " 同步方法开始"); System.out.println(Thread.currentThread().getName() + " 其他信息...... "); System.out.println(Thread.currentThread().getName() + " 同步方法结束"); } public void run() { show(); // 调用 show() 方法显示线程的相关信息 } }程序运行结果为:
线程A 同步方法开始
线程A其它信息......
线程A 同步方法结束
线程B 同步方法开始
线程B其它信息......
线程B 同步方法结束
相关文章
- Java synchronized实现线程同步
- Java synchronized:实现线程同步(附带实例)
- Java synchronized实现线程互斥(附带实例)
- Java synchronized底层实现原理详解(新手必看)
- Java synchronized可重入锁的底层实现(新手必看)
- Java synchronized关键字是怎样保证线程安全的?
- Java synchronized锁升级过程和原理(新手必看)
- Java synchronized锁的粗化和消除(新手必看)
- Java volatile和synchronized的区别(新手必看)
- Java中的可重入锁(synchronized和ReentrantLock)