`

线程安全扫盲贴二——锁详解

阅读更多

上篇提了一点:  由于内部锁是基于线程而非调用的,当一个线程执行increase方法时,已经拿到了counter对象引用的锁,那么其他线程执行同一个对象的increase或increase2会堵塞等待counter对象锁。

 

这里涉及到线程的重进入机制,如果线程A进入了increase,那么它已经拿到了counter的对象锁,那么该线程也可以进入increase2,因为这里的increase和increase2是同一把锁。

   重进入的原理 :JVM记录了锁所属的线程和该线程对锁的计数。比如我们在下面的increase方法中再调用同样对this加锁的increase2,那么这个线程在 increase方法执行时如果获得了锁,那么它可以进入increase2中,该锁的计数会变成2.当它推出increase2时,锁的计数值减1.当 一个锁的计数值为0时,认为该锁没有被任何线程占有。

 

如果一个类的成员变量在很多地方需要访问,不仅需要把访问的代码加入到synchronized块中,在任何地方,每次访问相同的变量时,需要同一个锁。

 

 

下面修改了一些方法,

package com.zyp.test.concurrent;

public class Counter {
	private int count = 0;
	public int increase(){
		synchronized (this) {
		       System.out.print("[increase]"+Thread.currentThread().getName()+" count="+count+" \n");
                       increase2();
                       return count++;
		}
	}
	public synchronized int increase2(){
		System.out.print("[increase2]"+Thread.currentThread().getName()+" count="+count+" \n");
		return count++;
	}
	public int increase3(){
		System.out.print("[increase3]"+Thread.currentThread().getName()+" count="+count+" \n");
		return count++;
	}
	
	public synchronized void increase4(){
		System.out.print("[increase4]"+Thread.currentThread().getName()+" start");
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.print("[increase4]"+Thread.currentThread().getName()+" end");
	}
	public int getCount() {
		return count;
	}
}

 

package com.zyp.test.concurrent.thread;

import com.zyp.test.concurrent.Counter;

public class CounterThread extends Thread{
	private Counter counter;
	public CounterThread(Counter counter){
		this.counter = counter;
	}
	
	public void run(){
		//此处测试三种increase方法
		counter.increase4();
		counter.increase3();
//		counter.increase2();
//		counter.increase3();
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
	}
	
	public static void main(String[] args){
		Counter counter = new Counter();
		for(int i = 0;i<20;i++){
			CounterThread t = new CounterThread(counter);
			t.start();
			if(i%3==0){
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

 截取一点打印的结果,对increase4加synchronized,对increase3不加时,出现了下面红色部分,线程Thread-3 拿到了counter的对象锁,但是Thread-1 在increase4执行的中途运行了,(Thread-3执行的是increase3而非increase4,increase3的方法并不需要锁,它当然可以执行)

******************************************
[increase4]Thread-1 start[increase4]Thread-1 end[increase4]Thread-3 start[increase3]Thread-1 count=1
[increase4]Thread-3 end
[increase3]Thread-3 count=2
[increase4]Thread-2 start[increase4]Thread-2 end[increase3]Thread-2 count=3

***********************************************

 

把调用increase3的改为调用increase2(即加上synchronized)后,就没有线程能在increase4执行的中途执行了。

因为要执行increase2的线程的对象锁与increase4是相同的,当一个线程占用后,其他的线程都无法执行increase4和increase2了。

***********************************************

[increase4]Thread-0 start[increase4]Thread-0 end[increase4]Thread-3 start[increase4]Thread-3 end[increase2]Thread-3 count=0
[increase4]Thread-1 start[increase4]Thread-1 end[increase2]Thread-1 count=1
[increase4]Thread-6 start[increase4]Thread-6 end[increase2]Thread-6 count=2

************************************************

 

java有两种并发的控制机制,一种是synchronized代码块,一种是可见性(volatile)

 

synchronized代码块 ,它包含了两部分:锁对象引用及被保护的代码块。

如increase方法,它的锁对象是当前的对象,保护的代码块是synchronized{}中的内容。

如increase2方法,它是将整个方法的代码作为了被保护的代码块。并且锁对象的引用则是调用该方法的对象。

因此此处的increase方法与increase2方法的意义是相同的 ,锁住的代码是相同的,使用的锁都是当前调用方法的对象。

 

    每个java对象都可以作为锁,这种this锁又叫内部锁(Intrinsic locks),或叫监控锁(monitor locks) 。要获取这种锁唯一的方法就是进入synchronized代码块或者synchronized申明的方法。正常或异常退出synchronized代码块或synchronized申明的方法后,锁被自动释放掉。内部锁在java中是排他锁,同一时刻最多只有一个线程可以持有该锁,其他想要获得该所的线程,要么等待,要么堵塞。

   成员变量的状态跟锁没有内在联系。被当做锁的使用的对象,并不影响其他线程访问对该对象。把该对象作为锁唯一做的一件事情,就是防止其他线程也对该对象加锁。 事实上每个对象都有个java内部创建的内部锁的原因,只是为了方便开发人员不用每次去显示的创建锁对象。

   我在代码中通过List lst = Collections.synchronizedList(new ArrayList())创建了一个线程安全的List,可以查看Collections类中有一个内部类SynchronizedCollection,这个SynchronizedCollection类专门创建了Object mutex,把mutex作为锁,把List的非线程安全的方法用mutex加锁重写了一遍:

	public boolean equals(Object o) {
	    synchronized(mutex) {return list.equals(o);}
        }
	public int hashCode() {
	    synchronized(mutex) {return list.hashCode();}
        }

	public E get(int index) {
	    synchronized(mutex) {return list.get(index);}
        }
	public E set(int index, E element) {
	    synchronized(mutex) {return list.set(index, element);}
        }
	public void add(int index, E element) {
	    synchronized(mutex) {list.add(index, element);}
        }
	public E remove(int index) {
	    synchronized(mutex) {return list.remove(index);}
        }

	public int indexOf(Object o) {
	    synchronized(mutex) {return list.indexOf(o);}
        }
	public int lastIndexOf(Object o) {
	    synchronized(mutex) {return list.lastIndexOf(o);}
        }

 

    有一种开发习惯是将在多线程中不稳定的成员变量封装到一个对象中,再在该对象中用该对象的内部锁加锁操作,如Vector的一些方法,就直接用了内部锁。

   public synchronized int capacity() {
	return elementData.length;
    }

    public synchronized int size() {
	return elementCount;
    }

    public synchronized boolean isEmpty() {
	return elementCount == 0;
    }

   这种开发习惯并没有得到JDK特别的支持,只是习惯而已。甚至这种线程安全的控制模式是很容易被破坏掉的,比如继承Vectory类,并提供一个没有synchronized关键字的方法来读取上面代码中的elementCount变量。

 

   在上述两例JDK的代码中可以看到,一个变量被一个锁保护了,那么所有对该变量的访问都需要持有这个锁,并且要保证在同一时刻只有一个线程能访问该变量。所以我上例中的increase3,在无获得锁的情况下,访问了被内部锁保护的count,造成了线程不安全的问题,是不对的,并且此处的increase3方法要执行应该要获得同其他increase方法同样的锁,才是正确的。

 

复合操作(Compount Action)

 除了这种count++,需要加锁,对于if doSometing这种复合操作也需要加锁,比如下面虽然getCount(),increase()都加了锁,但是当他们复合操作需要原子性,则需要对这个代码块加锁

if(getCount()>0){
   increase();
}

 

static synchronized使用的锁是调用该方法的类。

 

volatile 只能降低并发造成错误的几率,不能根本消除问题,对变量加volatile关键字后,线程在操作该变量时,不会将该变量复制到自己的线程的内存区中操作,而是直接操作所有线程共享的内存区域数据。相当于所有线程操作一个数据。

比如count++操作:

它有三步,如果不加volatile关键字,先读取内存的count值到线程工作区的内存,把线程的内存区的count值加1,把线程区结果值复制到内存区。

如果用synchronized锁住count++,那么可以保证多个线层执行时,只有一个线程完成了这三步后其他的才能继续count++。而使用volatile则没有了第一步和第三部,直接在内存中将count++了。这样减少了线程并发出错的概率,但是不能保证,当count是1时,线程1正在将其加一,此时线程2又来执行count++,线程2把执行完后将count设置为了2,此时线程1执行完了,再把count设置成了2,而我们希望的count应该是3。

 

 

——————————————————————————

《Java Concurrency Program》:

Every shared, mutable variable should be guarded by exactly one lock.
Make it clear to maintainers which lock that is.

 

 

分享到:
评论

相关推荐

    线程,同步与锁————Lock你到底锁住了谁?.htm

    线程,同步与锁————Lock你到底锁住了谁?.htm

    CVI 线程锁、线程安全变量实例

    在计算机编程领域,尤其是涉及到实时系统和并发编程时,线程锁和线程安全变量是至关重要的概念。LabWindows/CVI是一种流行的交互式C开发环境,特别适合于开发科学和工程应用。本实例将深入探讨如何在LabWindows/CVI...

    多线程———入门详解

    在多线程编程中,线程安全是一个重要的概念。由于多个线程可能同时访问和修改共享资源,如果不加以控制,可能会出现数据不一致、竞态条件等问题。为此,开发者需要使用锁、信号量等同步原语来保护共享数据,确保线程...

    并发编程——认识java里的线程(csdn)————程序.pdf

    并发编程——认识 Java 里面的线程 在 Java 编程中,并发编程是一个非常重要的概念。Java 程序天生就是多线程的,main 方法开始执行后,按照既定的代码逻辑执行,看似没有其他线程参与,但实际上 Java 程序天生就是...

    python进程、线程(csdn)————程序.pdf

    在Python编程中,进程和线程是并发...进程池和线程池是管理并发任务的有效工具,而进程锁和线程同步机制则是保证并发安全的重要手段。通过合理利用这些概念和工具,开发者可以在Python中构建出高效、可靠的多任务系统。

    多线程编程之二——MFC中的多线开

    ### 多线程编程之二——MFC中的多线程 在C++开发尤其是Windows平台下的应用程序开发中,多线程技术是不可或缺的一部分。本文将详细介绍如何在Microsoft Foundation Classes(MFC)框架下进行多线程编程。 #### MFC...

    多线程编程之二——MFC中的多线程开发

    第二个重载形式用于创建用户界面线程,需要一个指向`CWinThread`派生类的运行时类对象的指针,此类定义了线程的行为,包括启动和退出等。用户界面线程具备与主线程相似的消息机制。 `CWinThread`是MFC中代表线程的...

    python实现线程池并可自动拓展和减小线程数(csdn)————程序.pdf

    在Python编程中,线程池是一种管理多个线程并发执行任务的有效工具,它可以有效地调度和控制线程的数量,从而优化系统资源的使用。本篇将介绍如何使用Python实现一个线程池,该线程池还能根据设定的时间自动扩展和...

    多线程编程之二——MFC中的多线程开发-VC知识库文章.pdf

    《多线程编程之二——MFC中的多线程开发》 MFC(Microsoft Foundation Classes)是微软提供的一套面向对象的C++类库,它为Windows应用程序开发提供了丰富的支持,包括多线程编程。在MFC中,多线程编程主要分为两类...

    多线程编程之二——MFC中的多线开发

    总的来说,MFC中的多线程开发涉及线程创建、线程同步、线程通信和线程安全等多个方面。理解并熟练掌握这些概念和技术,对于构建高效、稳定的多线程MFC应用至关重要。通过不断实践和学习,你可以有效地利用多线程的...

    Java多线程之定时任务 以及 SpringBoot多线程实现定时任务——异步任务

    1. SpringBoot 自定义线程池以及多线程间的异步调用(@Async、@EnableAsync) 2.Java多线程之定时任务 以及 SpringBoot多线程实现定时任务 3.@EnableScheduling 与 @Scheduled

    多线程编程之二——MFC中的多线程开发.pdf

    MFC中的多线程开发还包括同步机制,比如使用临界区(CCriticalSection)、事件(CEvent)、互斥锁(CMutex)、信号量(CSemaphore)等来避免线程之间发生冲突和数据不一致的问题。这些同步类都是C++模板类,它们提供...

    多线程编程之三——线程间通讯

    线程间通信是多线程编程中的核心概念,它允许不同线程之间交换信息和协调工作。当一个程序中存在多个并发执行的线程时,它们可能需要共享数据或者互相通知事件的发生,这就需要线程间通信机制。下面将详细讨论几种...

    Java多线程——线程八锁案例分析.docx

    尽管如此,由于`synchronized`锁的存在,第二个线程仍然不能在`getOne()`执行期间执行`getTwo()`。因此,即使`getTwo()`没有睡眠,它也需要等待`getOne()`完成并释放锁后才能继续。运行结果反映了这一情况,`one`先...

    java多线程设计模式详解(PDF及源码)

    重点回顾 练习问题 Introduction 2 多线程程序的评量标准 多线程程序的评量标准 安全性——不损坏对象 生存性——进行必要的处理 复用性——可再利用类 性能——能快速、大量进行处理 评量标准的总结 重点回顾 练习...

    c++11 多线程编程——如何实现线程安全队列

    线程安全队列的接口文件如下: #include template class threadsafe_queue { public: threadsafe_queue(); threadsafe_queue(const threadsafe_queue&); threadsafe_queue& operator=(const threadsafe_queue&...

    java锁详解.pdf

    Java 锁详解 Java 锁是 Java 并发编程中的一种基本机制,用于确保线程安全和避免竞争条件。Java 锁可以分为两大类:synchronized 锁和 ReentrantLock 锁。 一、Synchronized 锁 1. 锁的原理:synchronized 锁是...

Global site tag (gtag.js) - Google Analytics