JAVA线程学习笔记(三)---线程同步(一)

 
      这一章主要介绍线程同步的相关知识,什么是线程同步呢?我理解的线程同步是:当两个线程需要共享一个公有的资源时,我们引入的一套机制,让在同一时间点,只有一个线程在使用公有资源。 
     但是,我们知道每个线程的运行的时机我们是无法控制的,那么我们怎么样保证在同一时间不会出现多个线程访问同一资源呢?
如下面的例子,由于我们不知道,CPU到底先为那个线程分配时间片,所以我们无法确定那个线程先被执行,但是我们希望每个线程的执行代码都具有原子性,即在一个线程执行完成之后,再进行第二个线程的执行。
 线程类代码:
public class MyTestThread extends Thread{

	private int startNum ;
	
	public int getStartNum() {
		return startNum;
	}
	public void setStartNum(int startNum) {
		this.startNum = startNum;
	}

	@Override
	public void run() {

		//线程只有一个功能就是打印startNum后面的5个数据
		for(int i = startNum ; i < startNum + 5 ; i++){
			
			System.out.println("nowNum is -----> " + i);
		}
	}

}
主函数代码:
public class Main {
 
	public static void main(String [] arg){
		
		MyTestThread newThread1 = new MyTestThread();
		MyTestThread newThread2 = new MyTestThread();
		newThread1.setStartNum(0);
		newThread2.setStartNum(5);
		newThread1.start();
		newThread2.start();
		
	}
}
运行结果为:
nowNum is -----> 0
nowNum is -----> 1
nowNum is -----> 5
nowNum is -----> 6
nowNum is -----> 7
nowNum is -----> 8
nowNum is -----> 9
nowNum is -----> 2
nowNum is -----> 3
nowNum is -----> 4


    从运行结果我们可以知道,打印的数据出现了交叉的现象,在一个线程执行的过程中,另外一个线程也有机会进行打印,而我们期望的是两个线程中的一个完成了打印,另外一个线程再继续打印,那么怎么样避免这种情况的产生呢,java给我们提供了一个关键字synchronized,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码,其实每个java对象都有一个锁,而使用synchronized,能够保证该锁对象在某一时间只能分配给一个特定的线程,其他线程只能等待。
   对于关键字synchronized需要注意的有以下几点:
   1.synchronized是利用封锁相应的资源的方式来实现共享资源的互斥访问;
    2.我们在使用synchronized的时候,需要知道我们封锁的是什么,他的作用范围是多大,针对的是该类的一个特定的对象,还是该类的所有对象,如下面的例子:
public class MyTestThread extends Thread{

	private int startNum ;
	public int getStartNum() {
		return startNum;
	}
	public void setStartNum(int startNum) {
		this.startNum = startNum;
	}

	@Override
	public synchronized void run() {

		//线程只有一个功能就是打印startNum后面的5个数据
		for(int i = startNum ; i < startNum + 5 ; i++){
				
				System.out.println("nowNum is -----> " + i);
			}
	}
}
public class Main {
 
	public static void main(String [] arg){
		
		MyTestThread newThread1 = new MyTestThread();
		MyTestThread newThread2 = new MyTestThread();
		newThread1.setStartNum(0);
		newThread2.setStartNum(5);
		newThread1.start();
		newThread2.start();
		
	}
}
运行结果为:
nowNum is -----> 0
nowNum is -----> 5
nowNum is -----> 1
nowNum is -----> 6
nowNum is -----> 2
nowNum is -----> 7
nowNum is -----> 3
nowNum is -----> 8
nowNum is -----> 4
nowNum is -----> 9


     这并不是我们想要的结果,我们想要的是一个线程打印完,另外的线程再开始打印,这是为什么呢?并不是随随便便加上一个synchronized关键字就能够实现线程的同步,这是因为当前的synchronized方法加在了run方法的前面,封锁住的是当前的对象,在main函数中我们生成了两个线程对象,这样每个线程在执行run方法时都持有了它自己的对象锁,没有其他线程和它竞争,当然不能通过这种方式来实现线程的同步。
      我们改变一下线程类的实现方式:
public class MyTestThread extends Thread{

	private int startNum ;
	
	private static Object lockObject = new Object();
	public int getStartNum() {
		return startNum;
	}
	public void setStartNum(int startNum) {
		this.startNum = startNum;
	}

	@Override
	public  void run() {

		synchronized (lockObject) {
			
			//线程只有一个功能就是打印startNum后面的5个数据
			for(int i = startNum ; i < startNum + 5 ; i++){
					
					System.out.println("nowNum is -----> " + i);
				}
		}
		
	}
}
这时的结果就是我们所需要的了:
nowNum is -----> 0
nowNum is -----> 1
nowNum is -----> 2
nowNum is -----> 3
nowNum is -----> 4
nowNum is -----> 5
nowNum is -----> 6
nowNum is -----> 7
nowNum is -----> 8
nowNum is -----> 9


当然在这里也可能出现两种结果,一种如上图从0打印到9,另外一种是从5打印到9,再从0打印到4,这是因为,我们不知道CPU到底先调用哪个线程,有可能线程1开始执行,也有可能是线程2开始执行,但是由于我们添加的synchronized同步块,并且针对的是类的所有对象,所以当一个线程对象获得了锁后,其他的线程就必须等待它执行完同步代码块,释放锁之后才能获得对应的锁并开始执行。
当然我们也可以采用下面的方法:
public class MyTestThread extends Thread{

	private int startNum ;
	
	private Object lockObject ;

	public MyTestThread(Object lockObject){
		
		this.lockObject = lockObject;
	}
	
	@Override
	public  void run() {

		synchronized (lockObject) {
			
			//线程只有一个功能就是打印startNum后面的5个数据
			for(int i = startNum ; i < startNum + 5 ; i++){
					
					System.out.println("nowNum is -----> " + i);
				}
		}
		
	}
	public int getStartNum() {
		return startNum;
	}
	
	public void setStartNum(int startNum) {
		this.startNum = startNum;
	}
}
public class Main {
 
	public static void main(String [] arg){
		
		Object lockObjiect = new Object();
		MyTestThread newThread1 = new MyTestThread(lockObjiect);
		MyTestThread newThread2 = new MyTestThread(lockObjiect);
		newThread1.setStartNum(0);
		newThread2.setStartNum(5);
		newThread1.start();
		newThread2.start();
		
	}
}


采用的上面的方法同样可以实现线程的同步,通过上面两个例子我们可以知道,线程同步的关键是,多个线程必须在竞争同一个共享资源,无论是生成一个对象,然后传递给每个线程,并且每个线程都对该对象使用synchronized修饰,实现对应代码块在该对象上同步;或者是通过对类的静态变量使用synchronized进行修饰来实现线程同步,其实质都是每个线程在竞争同一个锁资源。
当然,也可以把相关方法变成静态方法来实现多个线程竞争同一个锁资源(此时的锁资源是该类的锁),从而实现线程同步,因为加入静态方法是被所有类实例对象共享,因此线程对象在访问此静态方法时是互斥访问的,从而可以实现线程的同步。

3.使用synchronized保证一段代码在多线程执行时是互斥的,但是没有用synchronized修饰的代码不存在互斥的问题,在同一时间可以被多个线程访问
如下面的例子程序:

public class Command {

   public synchronized void printlnNumDesc(String threadName){
	   
	   for(int i=4 ; i >= 0 ; i--){
		   
		   System.out.println(threadName + "---------------> " + i);
		   
	   }
   }

   public  void printlnNumAsc(String threadName){
	   
	   for(int i = 5 ; i < 10 ; i++){
		   
		   System.out.println(threadName + "-----------> " + i);
		   
	   }
	   
	   
   }
}
public class Main {
 
	public static void main(String [] arg){
		
		Command command = new Command();
		MyTestThreadOne newThread1 = new MyTestThreadOne(command);
		MyTestThreadTwo newThread2 = new MyTestThreadTwo(command);
		newThread1.start();
		newThread2.start();
		
	}
}
public class MyTestThreadOne extends Thread{

	Command command ;
	public MyTestThreadOne(Command command){
		
		this.command = command;
		
	}
	@Override
	public  void run() {

		command.printlnNumDesc("Thread One");
		
	}
	
	
}
public class MyTestThreadTwo extends Thread{

	Command command ;
	public MyTestThreadTwo(Command command){
		
		this.command = command;
		
	}
	@Override
	public  void run() {

		command.printlnNumAsc("Thread Two");
	}
		
	
}
运行结果为:
Thread One---------------> 4
Thread One---------------> 3
Thread One---------------> 2
Thread Two-----------> 5
Thread Two-----------> 6
Thread Two-----------> 7
Thread Two-----------> 8
Thread Two-----------> 9
Thread One---------------> 1
Thread One---------------> 0
可以看出在线程1运行的过程中,线程2也可以执行相关的函数。
4. 每个对象都有一个标志锁,当一个线程访问了对象的某个synchronized数据(包括函数)时,这个对象就将被“上锁”,所有使用synchronized修饰的同一对象的代码块,函数都被锁住了,只有等待该线程执行完同步代码块,释放锁资源,其他线程才能重新获得锁,进行执行。如上例中,如果我们将Command类变成这样:
public class Command {

	
 
	public synchronized void printlnNumDesc(String threadName){
		  
		   for(int i=4 ; i >= 0 ; i--){
			   
			   System.out.println(threadName + "---------------> " + i);
			   
	   }
	   
   }

   public synchronized  void printlnNumAsc(String threadName){
	   
	   for(int i = 5 ; i < 10 ; i++){
		   
		   System.out.println(threadName + "-----------> " + i);
		   
	   }
	   
	   
   }
}
public class Command {

	public void printlnNumDesc(String threadName) {

		synchronized (this) {
			for (int i = 4; i >= 0; i--) {

				System.out.println(threadName + "---------------> " + i);

			}
		}
	}

	public void printlnNumAsc(String threadName) {

		synchronized (this) {

			for (int i = 5; i < 10; i++) {

				System.out.println(threadName + "-----------> " + i);

			}
		}

	}
}
上面两种实现方式是一样的效果
这时候运行结果为:
Thread One---------------> 4
Thread One---------------> 3
Thread One---------------> 2
Thread One---------------> 1
Thread One---------------> 0
Thread Two-----------> 5
Thread Two-----------> 6
Thread Two-----------> 7
Thread Two-----------> 8
Thread Two-----------> 9
可以看到这两个线程实现了同步,
5.对于一个类也有一个锁,所以对一个类的静态变量使用synchronized,可以将整个类的对象都锁定。
所以我们在使用线程同步时要弄清楚,加synchronized修饰的对象对应的锁的范围。
如以下两个例子:
public class Command {

	private Object lockObject = new Object();
	private Object lockObject2 = new Object();
	private Integer  lockNumber = 0 ;
	private Integer lockNumber2 = 0 ;
	
	public void printlnNumDesc(String threadName) {

		synchronized(lockNumber){
			for (int i = 4; i >= 0; i--) {

				System.out.println(threadName + "---------------> " + i);

			}
		}
		

	}

	public void printlnNumAsc(String threadName) {

		synchronized(lockNumber2){
			
			for (int i = 5; i < 10; i++) {

				System.out.println(threadName + "-----------> " + i);

			}
		}
		

	}
}
public class Command {

	private Object lockObject = new Object();
	private Object lockObject2 = new Object();
	private Integer  lockNumber = 0 ;
	private Integer lockNumber2 = 0 ;
	
	public void printlnNumDesc(String threadName) {

		synchronized(lockObject){
			for (int i = 4; i >= 0; i--) {

				System.out.println(threadName + "---------------> " + i);

			}
		}
		

	}

	public void printlnNumAsc(String threadName) {

		synchronized(lockObject2){
			
			for (int i = 5; i < 10; i++) {

				System.out.println(threadName + "-----------> " + i);

			}
		}
		

	}
}
他们的运行结果是不一样的。对一个不是static的对象实现synchronized,只能在同一对象的锁下实现同步,不同的对象在多个线程下依然可以同时访问。

6.因为进行同步会使其他线程,不能访问同步块,必须等待锁对象释放出来才能,所以使用同步会牺牲掉一定的性能的,所以我们必须在实际应用中好好的衡量两者的平衡点。通常我们不必同步类中所有的方法,只是针对那些必须要保证其原子性的方法或者代码块进行同步,以保证资源的正确性。
7.一个线程可以获得多个对象锁,所以在使用同步时要考虑死锁的出现请求,并消除死锁。
8.线程在sleep时,它所获得的所有锁对象是不会被释放的。
9.每个对象只有一个锁;当提到同步时,应该清楚在哪个对象上同步。

   总之,我们在对需要同步的代码使用synchronized,必须要确认当一个线程执行到同步代码块的时候,它获得的是哪个对象的锁,这时候能够阻塞的线程,只有那些正在访问同样加了synchronized修饰的,并且针对的是同一锁对象,我们在使用同步时,一定要弄清楚,在那个对象上同步,锁的范围是多大,是否会引起死锁。下章将继续研究java的线程同步。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值