JAVA并发编程3_线程同步之synchronized关键字

在上一篇博客里讲解了JAVA的线程的内存模型,见:JAVA并发编程2_线程安全&内存模型,接着上一篇提到的问题解决多线程共享资源的情况下的线程安全问题。

不安全线程分析

public class Test implements Runnable {
	private int i = 0;

	private int getNext() {
		return i++;
	}
	@Override
	public void run() { // synchronized
		while (true) {
			synchronized(this){
				if(i<10){
					System.out.println(getNext());
				}else
					break;
			}
		}
	}
	public static void main(String[] args) {
			Test t = new Test();
			Thread t1 = new Thread(t);
			Thread t2 = new Thread(t);
			t1.start();
			t2.start();
			Thread.yield();
	}
}

与之前的代码的区别在于run方法被synchronized关键字修饰。

根据上一篇博客的分析:多线程在访问共享资源的时候由于CPU轮流给每个任务分配其占用的时间,而CPU的调度是随机的,因此就会发生某个线程正在访问该变量的时候CPU却将时间片分发给了其他的线程,这样就会发生这样的现象:一个线程从主内存读取到某个变量的值还没来得及修改(或者修改后刷新主内存),另一个线程就获得了CPU的执行权,也从主内存读取改变量的值。当CPU执行权再次回到第一个线程的时候会接着之前的中断处执行(修改变量等),执行权回到第二个线程时却不能看到第一个线程中改变了的值。归结起来就是说违背了线程内存的可见性。避免上看起来产生第一种输出的可能顺序如下图所示(实际上可能的情况非常多,因为i++不是单个的原子操作):

i++对应下面的JVM指令,因此在期间另一个线程都可能会修改这个变量。

4: aload_0

5: iconst_0

6: putfield      #2                  // Field i:I

为了体现内存的可见性,synchronized关键字能使它保护的代码以串行的方式来访问(同一时刻只能由一个线程访问)。保证某个线程以一种可预测的方式来查看另一个线程的执行结果。

线程同步

JAVA提供的锁机制包括同步代码块和同步方法。

每个Java对象都可以用做一个实现同步的锁,这些所成为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock),一个线程进入同步带吗快之前会自动获得锁,并且推出同步带吗快时自动释放锁。获得内置锁的位移途径就是进入由这个锁保护的同步代码块或方法并且该锁还未被其他线程获得。

Java内置锁相当于互斥体(互斥锁),意味着最多有一个线程持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果线程B永远不释放锁,那么线程A将永远等待下去。

每次只能有一个线程执行内置锁保护的代码块,因此这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会相互干扰。

原子性的含义:一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码块。

千万注意:并不是说synchronized代码块或者synchronized方法是不可分割的整体,是原子的,因为,显然使用不同锁的话之间不存在互斥关系。

买票例子的引入

下面是模拟火车站卖票的程序,理论上是要将编号为1-10的票卖按照由大到小顺序卖出去,结果用两个窗口(线程)卖就出现了这样的结果,有些编号的票卖了两次,有些没卖出去,并且还有编号为0的票卖了出去。显然结果错误的。

public class Test implements Runnable {
	private int i = 10;
	private void sale(){
		while (true) {
			if (i >0){
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread() + "正在卖第" + i + "张票");
				i--;
			} else
break;
		}
	}

	@Override
	public void run() {
		sale();
	}

	public static void main(String[] args) {
			Test t = new Test();
			Thread t1 = new Thread(t);
			Thread t2 = new Thread(t);
			t1.start();
			t2.start();
			Thread.yield();
	}
}

出现这种结果的原因就是没有对多个线程共同访问的资源进行同步加锁。下面我们对其进行线程同步,达到想要的效果:

synchronized代码块:

synchronized (lock){
     //同步的代码
 }

lock必须是一个引用类型的变量。

使用synchronized同步代码块:

public class Test implements Runnable {
	private int i = 10;
	private void sale(){
		Object o = new Object();
		while (true) {
			synchronized(o){
				if (i >0){
					System.out.println(Thread.currentThread() + "正在卖第" + i + "张票");
					i--;
				}else
break;
			}
		}
	}

	@Override
	public void run() {
		sale();
	}

	public static void main(String[] args) {
			Test t = new Test();
			Thread t1 = new Thread(t);
			Thread t2 = new Thread(t);
			t1.start();
			t2.start();
			Thread.yield();
	}
} 

咦?使用了同步代码块了怎么结果还是不对呢??我们先看正确的同步:

public class Test implements Runnable {
	private int i = 10;
	Object o = new Object();// 通常使用:/*static*/ byte[] lock = new byte[0];
	private void sale(){
		while (true) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized(o){
				if (i >0){

					System.out.println(Thread.currentThread() + "正在卖第" + i + "张票");
					i--;
				}else
					break;
			}
		}
	}

	@Override
	public void run() {
		sale();
	}

	public static void main(String[] args) {
			Test t = new Test();
			Thread t1 = new Thread(t);
			Thread t2 = new Thread(t);
			t1.start();
			t2.start();
			Thread.yield();
	}

这里线程同步的原理是怎样的呢?因为任何一个Java对象都可以作为一个同步锁,上面代码的对象o就是一个同步锁。

一个线程执行到synchronized代码块,线程尝试给同步锁上锁,如果同步锁已经被锁,则线程不能获取到锁,线程就被阻塞;如果同步锁没被锁,则线程将同步锁上锁,并且持有该锁,然后执行代码块;代码块正常执行结束或者非正常结束,同步锁都将解锁。

所以线程执行同步代码块时,持有该同步锁。其他线程不能获取锁,就不能进入同步代码块(前提是使用同一把锁),只能等待锁被释放。

这时候回头看上上段代码中的同步代码块,由于两个线程使用的锁是不一样的(创建了两个对象),因此,就算线程A在执行同步代码块,当线程2获得CPU执行权时,检查到这个锁并未被其他线程锁定,因此不具有互斥性,不能达到线程同步的效果。

同步方法

将synchronized作为关键字修饰类的某个方法,这样该方法就变成了同步方法。

直接将sale函数改为synchronized方法的结果是虽然卖票不会乱序,但是只有一个线程在卖票。所以稍微做些调整:

public class Test implements Runnable {
	private int i = 10;
	private void sale(){
		while (true) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			f();
		}
	}
	private synchronized void f(){
		if (i >0){
			System.out.println(Thread.currentThread() + "正在卖第" + i + "张票");
			i--;
		}else
			return;
	}

	@Override
	public void run() {
		sale();
	}

	public static void main(String[] args) {
			Test t = new Test();
			Thread t1 = new Thread(t);
			Thread t2 = new Thread(t);
			t1.start();
			t2.start();
			Thread.yield();
	}
}

这时候的锁是哪个对象呢?

当修饰的方法是类方法时同步锁是该类对应的Class对象;

当修饰普通方法时,该同步锁是当前对象即this。

体会:不要滥用synchronized方法

在平时的编程中为了达到线程同步的目的,在不经认真思考的情况下,经常发生synchronized关键字的滥用,归根结底是没有理解同步的原理本质。

看下面的代码:

public class Test implements Runnable{
    @Override
    public void run() {
		f();
    }
    public synchronized void f(){
        System.out.println(this);
    }

    public static void main(String[] args) {
        Test t1=new Test();
        Test t2=new Test();
		// f()里面的代码无法达到同步的目的
        new Thread(t1).start();
        new Thread(t2).start();
    }
}
//Output
//[email protected]
//[email protected]

根据打印的结果也可以看出来函数f()是无法同步的,因为这两个线程使用了两个同步锁。这就告诉我们,并不要看到一个方法是synchronized的就想当然的认为它是同步方法就在不同的线程里随便调用。

注:上面的代码里面多次使用到了Thread.sleep(long)方法,是让当前线程睡眠一会,这个方法会让当前线程放弃CPU的执行权,处于Time Waiting状态,CPU不在为其分配时间片。由于机器的不同可能不容易出现我们期望的线程切换,目这样做就可以强制的让线程切换。

另外,在synchronized代码里面使用sleep无效。因为该线程sleep后CPU不在为其分配时间片,但是这个时候线程已经拿到了同步锁,即使睡到天荒地老,它也不会把同步锁交出去,别的线程得到了CPU执行却却苦于没有同步锁而被拒之门外。后面学习线程的状态会讲到这些。

会写代码不一定理解了,理解了不一定能给别人讲清楚。想把一个东西用文字表述清楚真的挺不容易。

时间: 2024-10-02 04:18:00

JAVA并发编程3_线程同步之synchronized关键字的相关文章

JAVA并发编程4_线程同步之volatile关键字

上一篇博客JAVA并发编程3_线程同步之synchronized关键字中讲解了JAVA中保证线程同步的关键字synchronized,其实JAVA里面还有个较弱的同步机制volatile.volatile关键字是JAVA中的轻量级的同步机制,用来将变量的更新操作同步到其他线程.从内存可见性的角度来说,写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块. 旧的内存模型:保证读写volatile都直接发生在main memory中. 在新的内存模型下(1.5)

Java并发编程1——线程状态、synchronized

以下内容主要总结自<Java多线程编程核心技术>,不定时补充更新. 一.线程的状态 Java中,线程的状态有以下6类:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED.各状态之间的关系可用下图表示: 二.常用方法介绍 1.thread.start()和thread.run()的区别 1 public static void main(String[] args) { 2 Thread t = new Thread(); 3 t

JAVA 并发编程-传统线程同步通信技术(四)

首先介绍几个概念: wait()方法 wait()方法使得当前线程必须要等待,等到另外一个线程调用notify()或者notifyAll()方法. 当前的线程必须拥有当前对象的monitor,也即lock,就是锁. 线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行. 要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或s

Java 并发编程中使用 ReentrantLock 替代 synchronized 关键字原语

Java 5 引入的 Concurrent 并发库软件包中,提供了 ReentrantLock 可重入同步锁,用来替代 synchronized 关键字原语,并可提供更好的性能,以及更强大的功能.使用方法也很简单: public final ReentrantLock lock=new ReentrantLock(); ...... try { lock.lock(); // 进入同步内容 .... } finally { lock.unlock(); // 必须在 finally 块中解锁,否

Java并发编程:线程的同步

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权.因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去.因此,一般情况下,当队列满时,会让生产者交出对

Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

Java并发编程系列[未完]: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程:线程间的协作(wait/notify/sleep/yield/join) 一.线程的状态 Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态). New:新建状态,当线程创建完成时为新

Java并发编程之多线程同步

线程安全就是防止某个对象或者值在多个线程中被修改而导致的数据不一致问题,因此我们就需要通过同步机制保证在同一时刻只有一个线程能够访问到该对象或数据,修改数据完毕之后,再将最新数据同步到主存中,使得其他线程都能够得到这个最新数据.下面我们就来了解Java一些基本的同步机制. Java提供了一种稍弱的同步机制即volatile变量,用来确保将变量的更新操作通知到其他线程.当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的.然而,在访问volatile变量时不会执行加锁操作

JAVA并发编程6_线程协作/生产者-消费者

前面通过同步锁来同步任务的行为,两个任务在交替访问共享资源的时候,可以通过使用同步锁使得任何时候只有一个任务可以访问该资源,见博客:线程同步之synchronized关键字.下面主要讲的是如何使任务彼此间可以协作,使得多个任务可以一起工作去解决木某个问题,因为有些问题中,某些部分必须在其他部分被解决之前解决,就像在餐厅服务员要端菜就必须有厨师做好了菜.在任务协作时,可以让任务自身挂起,直至某些外部条件发生变化,表示是时候让这个任务向前推动了为止. wait/notify wait方法会在等待外部