当使用多线程时,多个线程之间彼此相互干涉的问题也就出现了。比如多线程爬虫时,多个线程同时维护一个uri队列,这样就可能会存在冲突。例如线程A准备取uri_a,但是线程B突然就抢走了uri_a,再比如线程A正在读取uri_b,此时线程B也来读取uri_b了。
boolean 是原子的,但不一定是线程安全的。因为除了原子性还有可见性。
java中递增操作自身也需要多个步骤,所以不是原子操作,在递增过程中也可能被线程机制挂起。
防止这种冲突的方法就是当资源被一个任务使用时,给其加上锁。基本上所有并发模式解决线程冲突问题都是采用序列化访问共享资源的方案。
Java提供了关键字synchronized的形式,为防止资源冲突提供了内置支持。当任务要执行被synchronized关键字保护的代码片段时,它将检查锁是否可用,然后获取锁并执行代码、释放锁。
共享资源一般是以对象形式存在的内存片段,要控制对共享资源的访问,得先把它包装进一个对象,然后把所有要访问这个资源的方法标记成synchronized .如果某个任务处于对标记为synchronized的方法a()的调用中,那么这个线程从方法a()返回之前,任何想要调用方法a()所处的类A中其他被标记为synchronized方法的线程都会被阻塞。
在类中声明synchronized 方法的格式如下:
synchronized void f(){} synchronized void g(){}所有的对象(比如某个类)都将自动含有单一的锁,当在对象上调用其任意的synchronized 方法的时候,此对象都会被加锁,这时该对象上其他的synchronized 方法只有等前一个synchronized 方法被调用完毕并释放了锁之后才能被调用。例如:对于上面的方法,某个任务对对象调用了方法f(),对于同一个对象而言,只有等f()调用结束并释放了锁之后其他任务才能继续调用f()和g()。因此对于一个对象来说,其上所有的synchronized 方法共享一把锁。这可以防止多个任务同时访问被编码为对象的内存。这样提高了安全性但是牺牲了时间,因此 synchronized的锁是加在对象上面的。
使用并发时,将域一定要设置成private否则关键字就不能防止其他任务直接访问域,就会产生冲突。
在类中我们经常碰到一个类中即有synchronized方法也有synchronized static方法,那么这两种方法拥有的锁有什么区别呢?例如下面的类:
pulbic class Something(){ public synchronized void A(){} public synchronized void B(){} public static synchronized void SA(){} public static synchronized void SB(){} }在上面的四个方法中,A()和B()是对类的实例加锁,而SA()和SB()是对类本身加锁。这句话的意思是:对于类Something的每个实例对象都会有一把锁,调用A()和B()方法时获取的是对象的锁。此外对于类Something本身也有一把锁(作为Class 类本身的一部分),SA()和SB()方法调用时获取的是类的锁,所以不管是类Something的多少个实例还是Something本身静态调用SA()和SB()都只能存在一个synchronized static方法运行,只有前一个synchronized static运行结束后释放类的锁,其余的synchronized static方法才可以继续执行。所以synchronized static方法可以在类的范围内防止对static 数据的并发访问。
每个访问临界共享资源的方法都必须被同步,否则他们就不会正确的工作。
在Java线程中,原子操作也不一定是线程安全的,因为在多核处理器系统中各个核心之间变量不一定是具备可见性的。我们不能完全依赖于原子性,更应该使用同步机制。(优先使用同步机制)
原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。对于读取和写入除了long和double类型之外的基本类型变量的操作可以保证它们会被当做不可分(原子)的操作来操作内存。 但是JVM可以把64位的long和double类型变量的读取和写入分为两个32位来执行,这就产生了在一个读取和写入中间发生上下文切换,导致不同任务可能看到不同的结果。如果我们在定义long或则double时加上Volatile关键字就可以获得原子性。 但是不同的JVM可以提供的原子性保证不同,我们应该依赖于平台相关性,更多的使用同步来完全代替原子操作。
在多核处理器系统中相对于单处理器系统来说,可见性问题远比原子性问题要多。Java的内存模型中每个线程的工作空间内存和主内存的交互决定了存在可见性问题。一个任务做出的修改即使在不中断的意义上讲是原子性的,但是对于其他任务也可能是不可见的(例如修改只是暂时性的存储在本地处理器的缓存中,其他处理器上不可见),因此不同的任务对应用的状态有不同的视图。另一方面,同步机制强制在处理器系统中,一个任务的修改必须在应用中是可视的,所以同步可以确保可视性。
volatile关键字确保了应用中的可见性。
如果一个域被修饰成了volatile变量,那么只要对这个域产生了写操作(即使这里使用了处理器本地缓存),那么所有的读操作都可以看到这个修改。volatile域会被立即写入主存中,而读取操作就发生在主存中。
原子性和易变性的区别:在非volatile域上的原子操作不必刷新到主存中去,因此其他读取该域的任务也不必看到最新值。如果多个任务同时访问某个域,那么这个域就应该是volatile的,否则就只能通过同步机制来访问(同步也可以保证向主存中刷新)。如果一个域完全由synchronized方法或则语句块来防护那就不必设置成volatile的。
注意:如果一个域的值依赖于它之前的值(比如递增操作)或则某个域的值收到其他域的值得限制,volatile就无法正常工作。所以我们尽可能的使用同步机制而不使用原子操作和可视性。
使用volatile关键字而不是synchronized的唯一安全情况是:类中只有一个可变的域。
在java中自增不是原子操作(涉及一个读操作和一个写操作),通常对域中的值做赋值和返回操作通常都是原子性的。
note:基本上如果一个域可能被多个任务同时访问,或则这些任务中至少有一个是写入任务,那么这个域就应该设置成volatile的,表示告诉编译器不要执行任何移除读取和写入操作的优化,目的是用线程中的局部变量维护对这个域的精确同步,实际上读取和写入都是直接针对内存的,而不被缓存。
volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为volatile不会引起线程上下文的切换和调度。
volatile修饰的变量在进行写操作时,会多执行下面两步: - 将当前处理器JVM中缓存的数据强制写回系统的主内存; - 这个写回主内存的操作会使其他CPU中缓存了该内存地址数据的数据无效,需要重新从内存中重新读取。
synchronized实现同步基础:Java中的每一个对象都可以作为锁,具体有下面三种表现形式: 1. 对于普通同步方法,锁是当前实例对象; 2. 对于静态同步方法,锁是当前类的Class对象 3. 对于同步方法块,锁是synchronized括号里面的对象。
在JDK5中添加了一些原子性变量类,诸如:AtomicInteger、AtomicLong、AtomicReference。这些原子类提供了一些方法是机器级别的原子性。使用这些原子类就不必使用synchronized关键字了。
有时我们只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方法分离出来的代码段被称为临界区,也是用synchronized关键字类定义。注意;临界区的synchronized关键字被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制。
synchronized(ObjectLock){ }进入临界区前必须获得ObjectLock对象的锁,只有得到了锁才被允许进入临界区,否则会被阻塞。
使用临界区同步而不是整个方法同步可以明显提高性能。
synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是使用其方法正在被调用的当前对象:synchronized(this){}。在这种方式中,如果获得了synchronized块上面的锁,那么该对象其他的synchronized方法和临界区就不能被调用了。
有时必须在另一个对象上同步,那就必须保证所有相关任务都是在同一个对象上同步。
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,为使用相同变量的每个不同线程都创建不同的存储。例如:有5个线程都要使用变量X表示的对象,那线程本地存储就会自动生成5个用于x的不同的存储块。主要是可以将状态与线程关联起来。
