关于线程(四)线程同步

多线程程序中,由于同时有多个线程并发运行,有时会带来严重的问题,甚至引发错误。例如,一个银行帐户在同一时刻只能由一个用户操作,如果两个用户同时操作很可能会产生错误。为了解决这些问题,在多线程开发中就需要使用同步技术。

一、同步方法

同步方法是指用synchronized关键字修饰的方法,其与普通方法的不同是进入同步方法执行的线程将获得同步方法所属对象的锁,一旦对象被锁,其他线程就不能执行被锁对象的任何同步方法。也就是说,线程在执行同步方法之前,首先试图获得方法所属对象的锁,如果不能获得锁就进入对象的锁等待池等待,直到别的线程释放锁,其获得锁才能执行。

synchronized <返回类型> 方法名([参数列表]) [throws <异常序列>]
{
    //同步方法的方法体
}

在使用同步方法时要注意以下几点:
1、关键字synchronized只能标识方法,不能标识成员变量,不存在同步的成员变量。

2、一个对象可以同时有同步与非同步的方法,只有进入同步方法执行才需要获得锁,每个对象只有一个锁。若一个对象中有多个同步方法,当某线程在访问其中之一时,其他线程不能访问该对象中的任何同步方法,但可以访问非同步方法。

3、若线程获得锁后进入睡眠或进行让步,则将带着锁一起睡眠或让步,这种做法将严重影响等待锁的线程的执行,进而影响程序的整体性能。

4、同步方法退出时,锁将被释放,其他等待的线程可以获得锁。

5、同步将降低多线程程序的并发性,在一定程度上会影响性能。因此,在不需要同步的情况下不应该使用同步技术,以免降低性能。

6、静态方法也允许同步,这样就保证了静态方法不会同时被多个线程使用。

 

class Resource{
	synchronized void function1(Thread currThread){
		System.out.println(currThread.getName()+"线程执行function1方法!!!");
		try {
			Thread.sleep(1000);
			System.out.println(currThread.getName()+"线程睡醒了!!!");
		} catch(Exception e){
			e.printStackTrace();
		}
	}
	synchronized void function2(Thread currThread){
		System.out.println(currThread.getName()+"线程执行function2方法!!!");
	}	
}
//自定义线程类
class MyThread extends Thread{
	//资源对象的引用
	Resource rs;
	//构造器
	public MyThread(String tName,Resource rs){
		this.setName(tName);
		this.rs=rs;
	}
	public void run(){
		if(this.getName().equals("Thread1")){
			//如果线程名称是Thread1访问资源的function1方法
			rs.function1(this);			
		} else{
			//如果线程名称不是Thread1访问资源的function2方法
			System.out.println("Thread2启动,等待进入同步方法function2!!!");
			rs.function2(this);
		}
	}
}
//主类
public class Sample {
	public static void main(String args[]){
		 Resource rs=new Resource();
		 MyThread t1=new MyThread("Thread1",rs);
		 MyThread t2=new MyThread("Thread2",rs);
		 t1.start();
		 try{
			Thread.sleep(10);
		 }catch(Exception e){
			e.printStackTrace();
		 }         
		 t2.start();
	}
}

 

程序的执行顺序是:

1、Thread1首先启动执行,然后去睡眠,接着Thread2启动执行。

2、由于Thread2没有资源对象的锁,不能执行同步方法function2,只能等待。

3、Thread1睡眠时间结束,恢复执行,执行完毕退出function1后,释放资源对象的锁。

4、Thread2获取资源锁,成功执行同步方法function2,最后整个程序结束。

可以清楚看到程序的执行结果:

Thread1线程执行function1方法!!!

Thread2启动,等待进入同步方法function2!!!

Thread1线程睡醒了!!!

Thread2线程执行function2方法!!!

实际开发中要特别注意,同步的方法不能同时为抽象的方法。这是因为线程进入同步方法执行时要锁定方法所属的对象,而抽象的方法属于抽象类,抽象类是不能实例化的。

二、同步语句块

了解了同步的方法,我们应该会发现,同步的方法在同一时刻只能锁定一个对象(同步方法所属的对象),但在并发执行中有时需要锁定的对象远不止一个。另外,同步将影响程序的执行性能,应该尽量减少程序同步的区域,提高程序的并发性,而同步方法是无法精确指定同步区域的。

要想解决上述问题,就需要使用同步的语句块,其基本语法如下:

synchronized(<资源对象引用>){
    //需要进行同步的代码
}

1、圆括号中资源对象引用指向的是要被锁定的资源对象,同步语句块中编写应该是对资源进行操作的代码,这些代码同一时刻不允许多个线程执行。

2、同步语句块与同步方法很相似,线程必须获得对象的锁才能进入同步语句块执行,不过这里需要获得的是圆括号中资源对象引用所指向对象的锁。

3、同步语句块中线程一样可以调用资源对象的wait方法释放锁,进入等待池等待,一样可以使用notify、notifyAll方法通知等待池中的等待线程。

4、同步语句块执行结束后,锁被释放。

Java中资源对象的线程等待池不止一种,例如有调用wait方法进入的wait资源等待池,也有等待获得锁的锁等待池,锁等待池中的线程在锁被释放后会自动尝试再次获得锁,而wait资源等待池中的线程只有在wait时间到或收到通知才唤醒执行。当然,有可能线程在wait资源等待池中被唤醒后由于没有获得锁,又进入了锁等待池等待。

 

package com.dc;

class MyThread extends Thread {
	//该引用为资源对象
	private Object resource;
	//无参构造器
	public MyThread(){}
	//有参构造器
	public MyThread(Object resource,String name){
		//对线程进行初始化
		this.resource=resource;
		this.setName(name);
	}
	public void run(){
		//同步语句块
		synchronized(resource){
            System.out.println(this.getName()+"线程访问了资源!!!");
            System.out.println(this.getName()+"线程带着锁睡觉去了!!!");
            try{
            	Thread.sleep(1000);
            } catch(Exception e){
            	e.printStackTrace();
            }
            System.out.println(this.getName()+"线程带着锁睡醒后释放了锁!!!");
		}
	}
}
//主类
public class Sample{
	public static void main(String[] args){
		//创建资源对象
		Object resource=new Object();
		//创建2个线程
		MyThread mt1=new MyThread(resource,"MT1");
		MyThread mt2=new MyThread(resource,"MT2");
		//启动这2个线程
		mt1.start();
		mt2.start();
	}
}


1、MT1线程获得了资源对象的锁,访问了资源,然后其带着锁进入了睡眠状态,线程MT2由于没有获得锁只能进入资源对象的锁等待池等待。

 

2、MT1线程睡醒后执行完了同步语句块,释放了锁。

3、MT2线程获得了资源对象的锁,进入同步语句块执行,执行结束,整个程序退出。

最后的执行结果:

MT1线程访问了资源!!!
MT1线程带着锁睡觉去了!!!
MT1线程带着锁睡醒后释放了锁!!!
MT2线程访问了资源!!!
MT2线程带着锁睡觉去了!!!
MT2线程带着锁睡醒后释放了锁!!!

注:有的朋友可能会出现MT2先获得资源锁的情况,纯属正常情况,切勿惊讶。

三、线程的死锁

任何并发运行的系统中,当有多个访问者并发访问多个互斥的资源时,就有可能造成死锁的现象。如果系统的设计不完善,在Java中进行多线程开发时也有可能造成死锁。Java中死锁的含义是:“线程之间互相等待对方释放资源对象的锁,而每个线程又持有其他线程需要的锁,造成几个线程处在没有必要的永久等待当中,程序无法继续运行”。而且Java中没有自动检测死锁并排除死锁的功能,所以开发过程中要格外注意。

 

class MyThread extends Thread{
	//需要访问的两个资源对象引用
	private Object resource1;
	private Object resource2;
	//无参构造器
	public MyThread(){}
	//有参构造器
	public MyThread(Object resource1,Object resource2,String name){
		//传递资源对象的引用
		this.resource1=resource1;
		this.resource2=resource2;
		//设置线程的名称
		this.setName(name);
	}
	//重写run方法
	public void run(){
		//获得资源o1的锁,对资源o1进行操作
		synchronized(resource1){
			System.out.println(this.getName()+"线程拿到"+resource1+"对象的锁!!!");
			//睡眠10毫秒
			try{
				Thread.sleep(10);
			} catch(Exception e){	
			    e.printStackTrace();
			}
			System.out.println(this.getName()+"等待"+resource1+"对象的锁释放!!!");
			//在拿着资源o1锁的同时,需要拿到
			//资源o2的锁对两个资源同时进行操作
			synchronized(resource2){
				System.out.println(this.getName()+"线程拿到了"+resource2.toString()+"的锁!!!");
				System.out.println(this.getName() +"可以对两个资源同时操作了!!!");
			}
		}
	}
}
public class Sample{
	public static void main(String args[]){
		//创建3个资源对象
		String s1="tom";
		String s2="jerry";
		String s3="lucy";
		//创建3个线程对象
		MyThread mt1=new MyThread(s1,s2,"MT1");
		MyThread mt2=new MyThread(s2,s3,"MT2");
		MyThread mt3=new MyThread(s3,s1,"MT3");
		//启动上述3个线程
		mt1.start();
		mt2.start();
		mt3.start();
	}
}


三个现成分别首先成功地获得了一个对象的锁,然后再去请求其他线程已经锁定对象的锁,造成所有的线程都进入各自需要请求锁的对象的锁等待池等待。在没有获得请求对象的锁之前,也不会释放已经获得的锁,程序处于永久的,没有必要的等待当中。(以撸主的经验,模拟死锁有时候也挺难,不过不经意间写出死锁却容易得很。。)

 

四、volatile关键字

这个关键字我就想举个简单的例子来简单说明一下吧

以前的机器多为32位系统,很少有见64位系统,而Java的基本数据类型有两种是64位,long和double,由于32位系统只能分两次处理这样的数据,所以多线程并发处理数据就容易出现错误。例如,一个线程处理的数据的低32位,然而正在此时,另一个线程来读取该数据就只能读取到一个数据修改过程中的脏数据,这个数据既不是修改后的值,也不是修改前的值,所以是没有意义的。

因为volatile关键字只解决可见性与有序性,所以从原子性的角度来说,volatile不是最好的解决办法,但是long和doubled类型的数据可以在保证原子性的前提下进行赋值和运算,这是voliatile的一个特例,一定要保证原子性的操作,还要使用synchronized或者lock

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值