第21章 并发

1.基本上所有的并非模式在解决线程冲突问题时,都是采用序列化访问共享资源的方法。这种加锁访问机制,常常称为“互斥量”。

2.共享资源一般是以对象形式存在的内存片段,但也可以是文件,输入输出端口,或者打印机等。

3.所有对象都自动含有单一的锁(也称为监视器)。当在对象上调用其任意synchronized方法时,此对象就会被加锁。此时,不管是其它线程不管是调用对象的该同步方法,还是调用其它的同步方法,必须等待该锁释放后才能调用。

4.在使用并发时,将域设置为private是重要的,否则synchronized不能防止其它任务直接访问域。

5.一个线程可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一对象上的另一个方法,就会发生这种情况。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁,其计数为0。在线程第一次给对象加锁的时候,计数变为1。每次线程在这个对象上获得了锁,计数都会增加。显然,只有首先获得了锁的线程才能允许继续获取多个锁。每当线程离开一个synchronized方法,计数减少,当计数为零的时候,锁被完全释放,此时别的线程就可以使用此资源。

6.针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static方法可以在类的范围内防止对静态数据的并发访问。

7.每个访问临界共享资源的方法都必须同步,否则它们就不会正确工作。

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

而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。

使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。


个人总结: volatile只能保证可读性,不能保证原子性,即只要有一个线程修改它的时候就需要同步,再次提醒,第一选择应该用synchronized,这是最安全的方式,而其它任何方式都是由风险的。
public class TestThread {

private static volatile int stopstr=0;

private static void set(){
stopstr++;
stopstr++;
}

private static int get(){

return stopstr;
}

/**
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {

Thread th=new Thread(new Runnable() {

public void run() {

while(true){
set();
}

}
});

th.start();
TimeUnit.SECONDS.sleep(1);
while(true){
int i=get();
if(i%2!=0){
System.out.println(i);
break;
}
}
}
}

volatile一般最好不要用,影响性能;
volatile一般不能代替synchronised关键字,它只能保证可见性,不能保证互斥性。

不用volatile,也能保证可见性,例如:
public class TestThread {

public static void main(String[] args) throws InterruptedException {

A a=new A();
B b=new B(a);
C c=new C(a);
b.start();
c.start();
TimeUnit.SECONDS.sleep(5);
a.flag=true;

}
}


class A{
boolean flag=false;
}


class B extends Thread{
A a;
public B(A a){
this.a=a;
}
public void run(){
while(!a.flag){
System.out.println("B");
}
}
}


class C extends Thread{
A a;
public C(A a){
this.a=a;
}
public void run(){
while(!a.flag){
System.out.println("C");
}
}
}

从上面的运行结果可以看出,即使线程有副本,也能即使读到变量的更新。


10.原子性
具有原子性的操作被称为原子操作。原子操作在操作完毕之前不会线程调度器中断。在Java中,对除了long和double之外的基本类型的简单操作都具有原子性。简单操作就是赋值或者return。比如”a = 1;”和 “return a;”这样的操作都具有原子性。但是在Java中,上面买碘片例子中的类似”a += b”这样的操作不具有原子性,

所以如果add方法不是同步的就会出现难以预料的结果。在某些JVM中”a += b”可能要经过这样三个步骤:

1. 取出a和b
2. 计算a+b
3. 将计算结果写入内存

如果有两个线程t1,t2在进行这样的操作。t1在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1又把没有完成的第三步做完。这个时候就出现了错误,相当于t2的计算结果被无视掉了。所以上面的买碘片例子在同步add方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。类似的,像”a++”这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。
有一些并发大牛可以利用原子性避免同步而写出“免锁”的代码。Goetz开玩笑说:

如果你能编写出一个牛逼的高性能的JVM,你就可以考虑考虑是否可以避免使用同步。

所以,在成为这样牛的大牛之前,还是老老实实使用同步吧。

10.原子类
没有使用原子类时:
public class AtomicityTest implements Runnable {
private volatile int i = 0;

public int getValue() {
return i;
}

private synchronized void evenIncrement() {
i++;
i++;
}

public void run() {
while (true)
evenIncrement();
}

public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
AtomicityTest at = new AtomicityTest();
exec.execute(at);
while (true) {
int val = at.getValue();
if (val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}

使用原子类时:
public class AtomicIntegerTest implements Runnable {
private AtomicInteger i = new AtomicInteger(0);

public int getValue() {
return i.get();
}

private void evenIncrement() {
i.addAndGet(2);
}

public void run() {
while (true)
evenIncrement();
}

public static void main(String[] args) {
new Timer().schedule(new TimerTask() {
public void run() {
System.err.println("Aborting");
System.exit(0);
}
}, 5000); // Terminate after 5 seconds
ExecutorService exec = Executors.newCachedThreadPool();
AtomicIntegerTest ait = new AtomicIntegerTest();
exec.execute(ait);
while (true) {
int val = ait.getValue();
if (val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}

即对原子类操作,可以不用同步。对常规编程来说,它们很少会派上用场,但是涉及性能调优时,它们就大有用武之地了。

11. 在方法内部,用synchronized括起来的代码块,称为“临界区”,也被称为“同步控制块”。在进入临界区之前,必须获取括号中对象的锁。通过使用同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到大大提高。

运行例子后,性能排行:
同步控制块>lock锁>同步方法

12.synchronized必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象(this)。

13.有时必须在另一个对象上同步,但是如果你要这么做,就必须确保所有相关的任务都是在同一个对象上同步的。如果按照下面的例子做是不对的:
class DualSynch {
private Object syncObject = new Object();

public synchronized void f() {
for (int i = 0; i < 5; i++) {
System.out.println("f()");
Thread.yield();
}
}

public void g() {
synchronized (syncObject) {
for (int i = 0; i < 5; i++) {
System.out.println("g()");
Thread.yield();
}
}
}
}

public class SyncObject {
public static void main(String[] args) {
final DualSynch ds = new DualSynch();
new Thread() {
public void run() {
ds.f();
}
}.start();
ds.g();
}
}


14.线程本地存储,
ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是 threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量 (ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。

概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。例子:

public class Student {
private int age = 0; // 年龄

public Student(int age){
this.age=age;
}

public int getAge() {
return this.age;
}

public void setAge(int age) {
this.age = age;
}
}


import java.util.Random;
import java.util.concurrent.TimeUnit;

public class ThreadLocalDemo implements Runnable {

public static void main(String[] agrs) {
System.out.println(Test6.s.getAge());//线程对共享资源操作前,打印一下共享资源
ThreadLocalDemo td = new ThreadLocalDemo();
Thread t1 = new Thread(td, "a");
Thread t2 = new Thread(td, "b");
t1.start();
t2.start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Test6.s.getAge());//线程对共享资源操作完毕后,再打印一下共享资源,发现前后是一样的没有任何改变
}

public void run() {
accessStudent();
}

/**
* 示例业务方法,用来测试
*/
public void accessStudent() {
// 获取当前线程的名字
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running!");
// 产生一个随机数并打印
Random random = new Random();
int age = random.nextInt(100);
System.out
.println("thread " + currentThreadName + " set age to:" + age);
// 获取一个Student对象,并将随机数年龄插入到对象属性中
// Student student = getStudent();
Test6.value.get().setAge(age);
Thread.yield();
System.out.println("thread " + currentThreadName
+ " first read age is:" + Test6.value.get().getAge());
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("thread " + currentThreadName
+ " second read age is:" + Test6.value.get().getAge());
}
}

class Test6{
public static Student s=new Student(88);
public static ThreadLocal<Student> value = new ThreadLocal<Student>(){
protected synchronized Student initialValue() {
//创建本地存储,一定要覆盖该方法,返回一个共享变量。因为每个线程在get的时候,总会调用该方法,返回共享变量作为本地存储
return new Student(s.getAge());
}
};


}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值