java多线程开发的同步机制有哪些

如题所述

Java同步
标签: 分类:

一、关键字:

thread(线程)、thread-safe(线程安全)、intercurrent(并发的)

synchronized(同步的)、asynchronized(异步的)、

volatile(易变的)、atomic(原子的)、share(共享)

二、总结背景:

一次读写共享文件编写,嚯,好家伙,竟然揪出这些零碎而又是一路的知识点。于是乎,Google和翻阅了《Java参考大全》、《Effective Java Second Edition》,特此总结一下供日后工作学习参考。

三、概念:

1、 什么时候必须同步?什么叫同步?如何同步?

要跨线程维护正确的可见性,只要在几个线程之间共享非 final 变量,就必须使用 synchronized(或 volatile)以确保一个线程可以看见另一个线程做的更改。

为了在线程之间进行可靠的通信,也为了互斥访问,同步是必须的。这归因于java语言规范的内存模型,它规定了:一个线程所做的变化何时以及如何变成对其它线程可见。

因为多线程将异步行为引进程序,所以在需要同步时,必须有一种方法强制进行。例如:如果2个线程想要通信并且要共享一个复杂的数据结构,如链表,此时需要
确保它们互不冲突,也就是必须阻止B线程在A线程读数据的过程中向链表里面写数据(A获得了锁,B必须等A释放了该锁)。

为了达到这个目的,java在一个旧的的进程同步模型——监控器(Monitor)的基础上实现了一个巧妙的方案:监控器是一个控制机制,可以认为是一个
很小的、只能容纳一个线程的盒子,一旦一个线程进入监控器,其它的线程必须等待,直到那个线程退出监控为止。通过这种方式,一个监控器可以保证共享资源在
同一时刻只可被一个线程使用。这种方式称之为同步。(一旦一个线程进入一个实例的任何同步方法,别的线程将不能进入该同一实例的其它同步方法,但是该实例
的非同步方法仍然能够被调用)。

错误的理解:同步嘛,就是几个线程可以同时进行访问。

同步和多线程关系:没多线程环境就不需要同步;有多线程环境也不一定需要同步。

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。

互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。

可见性要更加复杂一些,documents它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题

小结:为了防止多个线程并发对同一数据的修改,所以需要同步,否则会造成数据不一致(就是所谓的:线程安全。如java集合框架中Hashtable和
Vector是线程安全的。我们的大部分程序都不是线程安全的,因为没有进行同步,而且我们没有必要,因为大部分情况根本没有多线程环境)。

2、 什么叫原子的(原子操作)?

Java原子操作是指:不会被打断地的操作。(就是做到互斥 和可见性?!)

那难道原子操作就可以真的达到线程安全同步效果了吗?实际上有一些原子操作不一定是线程安全的。

那么,原子操作在什么情况下不是线程安全的呢?也许是这个原因导致的:java线程允许线程在自己的内存区保存变量的副本。允许线程使用本地的私有拷贝进
行工作而非每次都使用主存的值是为了提高性能(本人愚见:虽然原子操作是线程安全的,可各线程在得到变量(读操作)后,就是各自玩
弄自己的副本了,更新操作(写操作)因未写入主存中,导致其它线程不可见)。

那该如何解决呢?因此需要通过java同步机制。

在java中,32位或者更少位数的赋值是原子的。在一个32位的硬件平台上,除了double和long型的其它原始类型通常都
是使用32位进行表示,而double和long通常使用64位表示。另外,对象引用使用本机指针实现,通常也是32位的。对这些32位的类型的操作是原
子的。

这些原始类型通常使用32位或者64位表示,这又引入了另一个小小的神话:原始类型的大小是由语言保证的。这是不对的。java语言保证的是原始类型的表
数范围而非JVM中的存储大小。因此,int型总是有相同的表数范围。在一个JVM上可能使用32位实现,而在另一个JVM上可能是64位的。在此再次强
调:在所有平台上被保证的是表数范围,32位以及更小的值的操作是原子的。

3、 不要搞混了:同步、异步

举个例子:普通B/S模式(同步)AJAX技术(异步)

同步:提交请求->等待服务器处理->处理完返回 这个期间客户端浏览器不能干任何事

异步:请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕

可见,彼“同步”非此“同步”——我们说的java中的那个共享数据同步(synchronized)

一个同步的对象是指行为(动作),一个是同步的对象是指物质(共享数据)。

4、 Java同步机制有4种实现方式:(部分引用网上资源)

① ThreadLocal ② synchronized( ) ③ wait() 与 notify() ④ volatile

目的:都是为了解决多线程中的对同一变量的访问冲突
ThreadLocal
ThreadLocal 保证不同线程拥有不同实例,相同线程一定拥有相同的实例,即为每一个使用该变量的线程提供一个该变量值的副本,每一个线程都可以独立改变自己的副本,而不是与其它线程的副本冲突。

优势:提供了线程安全的共享对象

与其它同步机制的区别:同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信;而 ThreadLocal 是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源,这样当然不需要多个线程进行同步了。

volatile
volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。
优势:这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
缘由:Java
语言规范中指出,为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原
始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而 volatile
关键字就是提示 VM :对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
使用技巧:在两个或者更多的线程访问的成员变量上使用 volatile 。当要访问的变量已在 synchronized 代码块中,或者为常量时,不必使用。

线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行A和B的同步,因此存在A和B不一致
的情况。volatile就是用来避免这种情况的。
volatile告诉jvm,它所修饰的变量不保留拷贝,直接访问主内存中的(读操作多时使用较好;线程间需要通信,本条做不到)

Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile
变量的最新值。Volatile
变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值
之间没有约束。

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

对变量的写操作不依赖于当前值;该变量没有包含在具有其他变量的不变式中。

sleep() vs wait()
sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

(如果变量被声明为volatile,在每次访问时都会和主存一致;如果变量在同步方法或者同步块中被访问,当在方法或者块的入口处获得锁以及方法或者块退出时释放锁时变量被同步。)
温馨提示:答案为网友推荐,仅供参考
第1个回答  2018-08-01
一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 java里边就是拿到某个同步对象的锁(一个对象只有一把锁); 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池 等待队列中)。 取到锁后,他就开始执行同步代码(被synchronized修饰的代码);线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中 等待的某个线程就可以拿到锁执行同步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。
众所周知,在Java多线程编程中,一个非常重要的方面就是线程的同步问题。
关于线程的同步,一般有以下解决方法:

1. 在需要同步的方法的方法签名中加入synchronized关键字。

2. 使用synchronized块对需要进行同步的代码段进行同步。

3. 使用JDK 5中提供的java.util.concurrent.lock包中的Lock对象。

另外,为了解决多个线程对同一变量进行访问时可能发生的安全性问题,我们不仅可以采用同步机制,更可以通过JDK 1.2中加入的ThreadLocal来保证更好的并发性。本回答被网友采纳
第2个回答  2016-02-29
Java的线程机制 摘 要: 多线程机制是Java的重要技术,阐述了线程和进程的差别;Java中线程4个状态之间的转换;并结合例子说明了两种创建线程的方法。 线程是指程序中能顺序执行的一个序列。一个线程只有一个入口点? 但可能有几个出口点? 不过,每个时刻的执行点总是只有一个。线程是不能独立运行的程序,而只是某个整体程序内部的一个顺序执行流。 多线程是Java的一个重要特点。如果一个程序是单线程的,那么,任何时刻都只有一个执行点。这种单线程执行方法使系统运行效率低,而且,由于必须依靠中断来处理输入/输出。所以,当出现频繁输入/输出或者有优先级较低的中断请求时,实时性就变得很差。多线程系统可以避免这个缺点。所谓多线程,就是通过系统的调度使几个具有不同功能的程序流即线程同时并行地运行。 在单处理器计算机系统中,实际上是不可能使多个线程真正并行运行的,而要通过系统用极短的时间、极快的速度对多个线程进行切换,宏观上形成多个线程并发执行的效果。 1 线程和进程机制上的差别 线程和进程很相象,它们都是程序的一个顺序执行序列,但两者又有区别。进程是一个实体,每个进程有自己独立的状态,并有自己的专用数据段,创建进程时, 必须建立和复制其专用数据段,线程则互相共享数据段。同一个程序中的所有线程只有一个数据段, 所以, 创建线程时不必重新建立和复制数据段。由于数据段建立和复制这方面的差异,使线程的建立和线程间的切换速度大大优于进程,另一方面,线程又具备进程的大多数优点。 假设银行系统办理存款和取款手续,将帐本看成数据段。如果按进程这种机制,那么,当储户去存/取款时,银行应先把帐本复制一遍,为储户建立一个独立的帐本再结算。如果按线程机制, 那么,银行里所有的出纳员都用同一个帐本,储户来办存/取款时,也从这个帐本直接结算。用线程机制省去了数据段复制这一步显然是线程独具的特点。 由于多个线程共享一个数据段,所以,也出现了数据访问过程的互斥和同步问题,这使系统管理功能变得相对复杂。 总的来说,一个多线程系统在提高系统的输入/输出速度、有效利用系统资源、改善计算机通信功能以及发挥多处理器硬件功能方面显示了很大优势。因此,一些最新的操作系统如Windows95、Windows98、Windows NT等都提供了对多线程的支持。但是,在多线程操作系统下设计多线程的程序仍然是一个比较复杂和困难的工作。由于需要解决对数据段的共享,所以,原则上应该从程序设计角度采用加锁和释放措施,稍有不慎,便会使系统产生管理上的混乱。 而Java从语言一级提供对多线程的支持,这样,可由语言和运行系统联合提供对共享数据段的管理功能和同步机制,使得多线程并行程序设计相对比较容易。 2 Java线程的生命周期 每个线程都是和生命周期相联系的,一个生命周期含有多个状态,这些状态间可以互相转化。 Java的线程的生命周期可以分为4个状态;创建(new)状态;可运行(runnable)状态;不执行(notrunnable)状态;消亡(dead)状态。 创建状态是指创建一个线程对应的对象的过程,Java系统中,些对象都是从Java.lang包内一个称为Thread的类用关键字new创建的。刚创建的线程不能执行,必须向系统进行注册、分配必要的资源后才能进入可运行状态,这个步骤是由start操作完成的,而处于可运行状态的线程也未必一定处于运行中,它有可能由于外部的I/O请求而处于不运行状态。进入消亡状态后,此线程就不再存在了。 一个线程创建之后,总是处于其生命周期的4个状态之一中,线程的状态表明此线程当前正在进行的活动,而线程的状态是可以通过程序来进行控制的,就是说,可以对线程进行操作来改变状态。 这些操作包括启动(start)、终止(stop)、睡眠(sleep)、挂起(suspend)、恢复(resume)、等待(wait)和通知(notify)。每一个操作都对应了一个方法? 这些方法是由软件包Java.lang提供的。通过各种操作,线程的4个状态之间可按图1所示进行转换。 2.1 创建(new)状态 如果创建了一个线程而没有启动它,那么,此线程就处于创建状态。比如,下述语句执行以后,使系统有了一个处于创建状态的线程myThread:? Thread myThread=new MyThreadClass(); 其中,MyThreadClass()是Thread的子类,而Thread是由Java系统的Java.lang软件包提供的。处于创建状态的线程还没有获得应有的资源,所以,这是一个空的线程,线程只有通过启动后,系统才会为它分配资源。对处于创建状态的线程可以进行两种操作: 一是启动(start)操作,使其进入可运行状态;二是终止(stop)操作,使其进入消亡状态。如果进入到消亡状态,那么,此后这个线程就不能进入其它状态,也就是说,它不复存在了。 start方法是对应启动操作的方法,其具体功能是为线程分配必要的系统资源,将线程设置为可运行状态,从而可以使系统调度这个线程。 2.2 可运行(runnable)状态 如果对一个处于创建状态的线程进行启动操作,则此线程便进入可运行状态。比如,用下列语句? myThread.start();? ?? 则使线程myThread进入可运行状态。上述语句实质上是调用了线程体即run()方法,注意,run()方法包含在myThread线程中,也就是先由java.lang包的Thread类将run()方法传递给子类MyThreadClass(),再通过创建线程由子类MyThreadClass,传递给线程myThread。 线程处于可运行状态只说明它具备了运行条件,但可运行状态并不一定是运行状态,因为在单处理器系统中运行多线程程序,实际上在一个时间点只有一个线程在运行,而系统中往往有多个线程同时处于可运行状态,系统通过快速切换和调度使所有可运行线程共享处理器,造成宏观上的多线程并发运行。可见,一个线程是否处于运行状, 除了必须处于可运行状态外,还取决于系统的调度。 在可运行状态可以进行多种操作,最通常的是从run()方法正常退出而使线程结束,进入消亡状态。 此, 还可以有如下操作? 挂起操作,通过调用suspend方法来实现; 睡眠操作,通过调用sleep方法来实现; 等待操作,通过调用wait方法来实现; 退让操作,通过调用yield方法来实现; 终止操作,通过调用stop方法来实现。 前面三种操作都会使一个处于可运行状态的线程进入不可运行状态。比如,仍以myThread线程为例,当其处于可运行状态后,再用如下语句? myThread.sleep (5000); 则调用sleep方法使myThread线程睡眠5s(5000ms)。这5s内,此线程不能被系统调度运行,只有过5s后,myThread线程才会醒来并自动回到可运行状态。 如果一个线程被执行挂起操作而转到不可运行状态,则必须通过调用恢复(resume)操作,才能使这个线程再回到可运行状态。 退让操作是使某个线程把CPU控制权提前转交给同级优先权的其他线程。 对可运行状态的线程也可以通过调用stop方法使其进入消亡状态。 2.3 不可运行(not runnable)状态 不可运行状态都是由可运行状态转变来的。一个处于可运行状态的线程,如果遇到挂起(suspend)操作、睡眠(sleep)操作或者等待(wait)操作,就会进入不可运行状态。 另外,如果一个线程是和I/O操作有关的,那么,在执行I/O指令时,由于外设速度远远低于处理器速度而使线程受到阻, 从而进入不可运行状态,只有外设完成输入/输出之后,才会自动回到可运行状态。线程进入不可运行状态后,还可以再回到可运行状态,通常有三种途径使其恢复到可运行状态。 一是自动恢复。通过睡眠(sleep)操作进入不可运行状态的线程会在过了指定睡眠时间以后自动恢复到可运行状态,由于I/O阻塞而进入不可运行状态的线程在外设完成I/O操作后,自动恢复到可运行状态。 二是用恢复(resume)方法使其恢复。如果一个线程由于挂起(suspend)操作而从可运行状态进入不可运行状态,那么,必须用恢复(resume)操作使其再恢复到可运行状态。 三是用通知(notify或notifyAll)方法使其恢复。如果一个处于可运行状态的线程由于等待(wait)操作而转入不可运行状态,那么,必须通过调用notify方法或notifyAll方法才能使其恢复到可运行状态,采用等待操作往往是由于线程需要等待某个条件变量,当获得此条件变量后,便可由notify或ontifyAll方法使线程恢复到可运行状态。 恢复到可运行状态的每一种途径都是有针对性的,不能交叉。比如,对由于阻塞而进入不可运行状态的线程采用恢复操作将是无效的。 在不可运行状态,也可由终止(stop)操作使其进入消亡状态。 2.4 消亡(dead)状态 一个线程可以由其他任何一个状态通过终止(stop)操作而进入消亡状态。 线程一旦进入消亡状态,那它就不再存在了,所以也不可能再转到其它状态。 通常,在一个应用程序运行时,如果通过其它外部命令终止当前应用程序,那么就会调用(stop)方法终止线程。但是,最正常、最常见的途径是由于线程在可运行状态正常完成自身的任务而″寿终正寝″,从而进入消亡状态,这个完成任务的动作是由run方法实现的。 3 Java线程的两种创建途径 一种途径是通过对Thread的继承来派生一个子类,再由此子类生成一个对象来实现线程的创建,这是比较简单直接的办法。Thread类包含在系统API提供的8个软件包之一Java.lang中,Thread类中包含了很多与线程有关的方, 其中,一个名为run的方法就是用来实现线程行为的。比如:? 1 import java.lang.*? //引用lang包 2 class Mango exteds Thread { 3 public void run() { 4 ...... 5 ?} 6 ?} 上述程序段中,第1行语句引用软件包lang,这样做是为了给编译器一个信息,从而使后面程序中有关lang包中的方法可直接用方法名,而不必带前缀“Java.lang”。第2行语句是从lang包Thread派生一个子类Mango, 而这个子类中提供了run方法的实现,这样,运行时,将由子类Mango 的 run方法置换父类Thread的run方法。 不过这一步还没有创建线, 必须由子类生成一个对象,并且进行启动操作,这样才能得到一个处于可运行状态的线程。生成对象其实就是完成线程的创建,而启动是对已创建的线程进行操作。具体语句如下:? Mango t=new Mango(); ?? t.start(); ?? 上面先用关键字new使线程进入创建状态,又调用start()方法使线程进入可运行状态。注意,start()方法是由Thread继承给子类Mango、然后又在生成对象时由对象t从类Mango得到的。 另一种途径是通过一个类去继承接口runnable来实现线程的创建? 而这个类必须提供runnable接口中定义的方法run()的实现。runnable是Java.lang包中的一个接口,在runnable接口中,只定义了一个抽象方法run()。所以,如用这种途径来创建线程,则应先由一个类连接接口runnable,并且提供run()方法的实现。比如,下面的程序段实现了与接口的连接。 1 public class xyz implements Runnable{ 2 int i; ? 3 public voed run(){ 4 while (true){? ? 5 System.out.println("Hello"+i++); 6 ? } 7 ? } 8 ? } 然后再创建一个线程? runnable r=new xyz(); ?? Thread t=new Thread(r); 这种途径创建线程比第一种途径灵活。当一个类既需要继承一个父类又要由此创建一个线程时,由于Java不支持多重继承,这样,用第一种途径将行不通,因为,按此思路创建线程也是以继承的方法实现的。 于是,就需要一个类既继承Thread类,又继承另一个父类。但用接口方法却能实现这个目标。 4 线程的启动和终止 Thread的start()方法对应于启动操作,它完成两方面的功能:一方面是为线程分配必要的资源,使线程处于可运行状态,另一方面是调用线程的run()方法置换Thread的中run()方法或者置换runnable中的run()方法来运行线程。 使用start()方法的语句很简单,即: ThreadName.start(); 下面的程序段先创建并启动线程myThread, 然后使用sleep()方法让其睡眠20000ms即20s,使其处于不可运行状态,过20s后,线程又自动恢复到可运行状态。 Thread MyThread=new MyThreadClass(); MyThread.start();? ?? try{ ? MyThread.sleep(20000); ?} catch(InterrujptedException e){ } ?
相似回答