java同步通信方式_Java之线程安全中的三种同步方式

一个程序在运行起来时,会转换为进程,通常含有多个线程。

通常情况下,一个进程中的比较耗时的操作(如长循环、文件上传下载、网络资源获取等),往往会采用多线程来解决。

比如,现实生活中,银行取钱问题、火车票多个窗口售票问题等,通常会涉及并发问题,从而需要用到多线程技术。

当进程中有多个并发线程进入一个重要数据的代码块时,在修改数据的过程中,很有可能引发线程安全问题,从而造成数据异常。例如,正常逻辑下,同一个编号的火车票只能售出一次,却由于线程安全问题而被多次售出,从而引起实际业务异常。

接下来,我以售票问题,来演示多线程问题中对核心数据保护的重要性。我们先来看不对多线程数据进行保护时会引发什么样的状况。

/**

* 售票问题

*/

public class Test1 {

static int tickets=10;

class SellTickets implements Runnable{

@Override

public void run() {

// 未加同步时,产生脏数据

while(tickets>0){

System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 张票");

tickets--;

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

if(tickets<=0){

System.out.println(Thread.currentThread().getName()+" -->售票结束!");

}

}

}

public static void main(String[] args) {

SellTickets sell=new Test1().new SellTickets();

Thread t1=new Thread(sell, "1号窗口");

Thread t2=new Thread(sell, "2号窗口");

Thread t3=new Thread(sell, "3号窗口");

Thread t4=new Thread(sell, "4号窗口");

t1.start();

t2.start();

t3.start();

t4.start();

}

}

上述代码运行后,效果如下:

1号窗口 -->售出第 10 张票

3号窗口 -->售出第 10 张票

2号窗口 -->售出第 10 张票

4号窗口 -->售出第 10 张票

3号窗口 -->售出第 6 张票

2号窗口 -->售出第 6 张票

1号窗口 -->售出第 5 张票

4号窗口 -->售出第 3 张票

3号窗口 -->售出第 2 张票

2号窗口 -->售出第 2 张票

1号窗口 -->售出第 2 张票

4号窗口 -->售票结束!

3号窗口 -->售票结束!

1号窗口 -->售票结束!

2号窗口 -->售票结束!

上述运行结果中,第10张票被售出多次,显然不符合实际应用中的逻辑。由于多线程调度中的不确定性,读者在演示上述代码时,可能会取得不同的运行结果。

为了解决上述脏数据的问题,我为大家介绍3种使用比较普遍的三种同步方式。

第一种,同步代码块。

有synchronized关键字修饰的语句块,即为同步代码块。同步代码块会被JVM自动加上内置锁,从而实现同步。

我们来看代码:

/**

* 售票问题

* @author 李章勇

*

*/

public class Test2 {

static int tickets=10;

class SellTickets implements Runnable{

@Override

public void run() {

//同步代码块

while(tickets>0){

synchronized(this){

if(tickets<=0){

break;

}

System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 张票");

tickets--;

}

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

if(tickets<=0){

System.out.println(Thread.currentThread().getName()+" -->售票结束!");

}

}

}

public static void main(String[] args) {

SellTickets sell=new Test2().new SellTickets();

Thread t1=new Thread(sell, "1号窗口");

Thread t2=new Thread(sell, "2号窗口");

Thread t3=new Thread(sell, "3号窗口");

Thread t4=new Thread(sell, "4号窗口");

t1.start();

t2.start();

t3.start();

t4.start();

}

}

上述代码运行结果:

1号窗口 -->售出第 10 张票

3号窗口 -->售出第 9 张票

4号窗口 -->售出第 8 张票

2号窗口 -->售出第 7 张票

3号窗口 -->售出第 6 张票

4号窗口 -->售出第 5 张票

2号窗口 -->售出第 4 张票

1号窗口 -->售出第 3 张票

4号窗口 -->售出第 2 张票

3号窗口 -->售出第 1 张票

1号窗口 -->售票结束!

2号窗口 -->售票结束!

4号窗口 -->售票结束!

3号窗口 -->售票结束!

通过运行结果可知,上述运行结果正常。

第二种,同步方法 。

即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

我们来看代码:

/**

* 售票问题

* @author 李章勇

*

*/

public class Test3 {

static int tickets=10;

class SellTickets implements Runnable{

@Override

public void run() {

//同步方法

while(tickets>0){

synMethod();

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

if(tickets<=0){

System.out.println(Thread.currentThread().getName()+" -->售票结束!");

}

}

//同步方法

synchronized void synMethod(){

synchronized(this){

if(tickets<=0){

return;

}

System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 张票");

tickets--;

}

}

}

public static void main(String[] args) {

SellTickets sell=new Test3().new SellTickets();

Thread t1=new Thread(sell, "1号窗口");

Thread t2=new Thread(sell, "2号窗口");

Thread t3=new Thread(sell, "3号窗口");

Thread t4=new Thread(sell, "4号窗口");

t1.start();

t2.start();

t3.start();

t4.start();

}

}

上述代码运行结果:

1号窗口 -->售出第 10 张票

4号窗口 -->售出第 9 张票

3号窗口 -->售出第 8 张票

2号窗口 -->售出第 7 张票

1号窗口 -->售出第 6 张票

2号窗口 -->售出第 5 张票

4号窗口 -->售出第 4 张票

3号窗口 -->售出第 3 张票

4号窗口 -->售出第 2 张票

3号窗口 -->售出第 1 张票

1号窗口 -->售票结束!

4号窗口 -->售票结束!

2号窗口 -->售票结束!

3号窗口 -->售票结束!

上述代码运行结果也正常。

第三种,Lock锁机制。

通过创建Lock对象,采用lock()加锁,采用unlock()解锁,来保护指定代码块。我们看如下代码:

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

/**

* 售票问题

* @author 李章勇

*

*/

public class Test4 {

static int tickets=10;

class SellTickets implements Runnable{

Lock lock=new ReentrantLock();

@Override

public void run() {

//Lock锁机制

while(tickets>0){

try{

lock.lock();

if(tickets<=0){

break;

}

System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 张票");

tickets--;

}finally{

lock.unlock();

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

if(tickets<=0){

System.out.println(Thread.currentThread().getName()+" -->售票结束!");

}

}

}

public static void main(String[] args) {

SellTickets sell=new Test4().new SellTickets();

Thread t1=new Thread(sell, "1号窗口");

Thread t2=new Thread(sell, "2号窗口");

Thread t3=new Thread(sell, "3号窗口");

Thread t4=new Thread(sell, "4号窗口");

t1.start();

t2.start();

t3.start();

t4.start();

}

}

运行结果如下:

1号窗口 -->售出第 10 张票

2号窗口 -->售出第 9 张票

3号窗口 -->售出第 8 张票

4号窗口 -->售出第 7 张票

1号窗口 -->售出第 6 张票

4号窗口 -->售出第 5 张票

2号窗口 -->售出第 4 张票

3号窗口 -->售出第 3 张票

1号窗口 -->售出第 2 张票

2号窗口 -->售出第 1 张票

3号窗口 -->售票结束!

1号窗口 -->售票结束!

2号窗口 -->售票结束!

4号窗口 -->售票结束!

最后总结:

由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否;而ReentrantLock是使用代码实现的,系统无法自动释放锁,需要在代码中的finally子句中显式释放锁lock.unlock()。

另外,在并发量比较小的情况下,使用synchronized是个不错的选择;但是在并发量比较高的情况下,其性能下降会很严重,此时ReentrantLock是个不错的方案。

补充:

在使用synchronized 代码块时,可以与wait()、notify()、nitifyAll()一起使用,从而进一步实现线程的通信。

其中,wait()方法会释放占有的对象锁,当前线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序;线程的sleep()方法则表示,当前线程会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁,也就是说,在休眠期间,其他线程依然无法进入被同步保护的代码内部,当前线程休眠结束时,会重新获得cpu执行权,从而执行被同步保护的代码。

wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会释放对象锁。

notify()方法会唤醒因为调用对象的wait()而处于等待状态的线程,从而使得该线程有机会获取对象锁。调用notify()后,当前线程并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM会在等待的线程中调度一个线程去获得对象锁,执行代码。

需要注意的是,wait()和notify()必须在synchronized代码块中调用。

notifyAll()是唤醒所有等待的线程。

接下来,我们通过下一个程序,使得两个线程交替打印“A”和“B”各10次。请见下述代码:

public class Test5 {

static final Object obj=new Object();

//一个子线程

static class ThreadA implements Runnable{

@Override

public void run() {

int count=10;

while(count>0){

synchronized(Test5.obj){

System.out.println("A-->"+count);

count--;

Test5.obj.notify();

try {

Test5.obj.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

}

//另一个子线程

static class ThreadB implements Runnable{

@Override

public void run() {

int count=10;

while(count>0){

synchronized(Test5.obj){

System.out.println("B-->"+count);

count--;

Test5.obj.notify();

try {

Test5.obj.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

}

public static void main(String[] args) {

new Thread(new ThreadA()).start();

new Thread(new ThreadB()).start();

}

}

显示结果如下:

A-->10

B-->10

A-->9

B-->9

A-->8

B-->8

A-->7

B-->7

A-->6

B-->6

A-->5

B-->5

A-->4

B-->4

A-->3

B-->3

A-->2

B-->2

A-->1

B-->1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值