java多线程及同步那些事

一.多线程的实现方式
1.继承Thread类重写run()方法
2.实现Runnable接口,new Thread(Runnable接口)
3.实现Callable接口通过FutureTask包装器来创建Thread线程
4.使用ExecutorService、Callable、Future实现有返回结果的线程ExecutorService、Callable、Future三个接口实际上都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。而且自己实现了也可能漏洞百出。
可返回值的任务必须实现Callable接口。类似的,无返回值的任务必须实现Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。注意:get方法是阻塞的,即:线程无返回结果,get方法会一直等待。再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:

import java.util.concurrent.*;  
import java.util.Date;  
import java.util.List;  
import java.util.ArrayList;  

/** 
* 有返回值的线程 
*/  
@SuppressWarnings("unchecked")  
public class Test {  
public static void main(String[] args) throws ExecutionException,  
    InterruptedException {  
   System.out.println("----程序开始运行----");  
   Date date1 = new Date();  

   int taskSize = 5;  
   // 创建一个线程池  
   ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
   // 创建多个有返回值的任务  
   List<Future> list = new ArrayList<Future>();  
   for (int i = 0; i < taskSize; i++) {  
    Callable c = new MyCallable(i + " ");  
    // 执行任务并获取Future对象  
    Future f = pool.submit(c);  
    // System.out.println(">>>" + f.get().toString());  
    list.add(f);  
   }  
   // 关闭线程池  
   pool.shutdown();  

   // 获取所有并发任务的运行结果  
   for (Future f : list) {  
    // 从Future对象上获取任务的返回值,并输出到控制台  
    System.out.println(">>>" + f.get().toString());  
   }  

   Date date2 = new Date();  
   System.out.println("----程序结束运行----,程序运行时间【"  
     + (date2.getTime() - date1.getTime()) + "毫秒】");  
}  
}  

class MyCallable implements Callable<Object> {  
private String taskNum;  

MyCallable(String taskNum) {  
   this.taskNum = taskNum;  
}  

public Object call() throws Exception {  
   System.out.println(">>>" + taskNum + "任务启动");  
   Date dateTmp1 = new Date();  
   Thread.sleep(1000);  
   Date dateTmp2 = new Date();  
   long time = dateTmp2.getTime() - dateTmp1.getTime();  
   System.out.println(">>>" + taskNum + "任务终止");  
   return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";  
}  
}

代码说明:
上述代码中Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。
参考文章
二.线程同步
(1)java内存模型
java内存模型
可以看出,线程对共享变量的读写都必须在自己的工作内存中进行,而不能直接在主内存中读写。不同线程不能直接访问其他线程的工作内存中的变量,线程间变量值的传递需要主内存作为桥梁。
那么就有个不同线程间共享变量的同步问题,要解决同步问题就需要满足可见性、原子性、有序性.
即:
可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。
有序性:程序执行的顺序按照代码的先后顺序执行。
只有满足这三个条件才能做到变量在多线程的同步,不管我们用什么synchronized还是什么volatile,lock最终目的都是满足这三个条件才能保证线程同步安全问题。
二,线程同步方式
1.synchronized相关文章同时满足了可见性、原子性、有序性故线程安全
2.volatile只满足了可见性,不能保证原子性故不能完全保证线程安全
3.lock锁如ReentrantLock同时满足可见性、原子性、有序性

//只给出要修改的代码,其余代码与上同
        class Bank {

            private int account = 100;
            //需要声明这个锁
            private Lock lock = new ReentrantLock();
            public int getAccount() {
                return account;
            }
            //这里不再需要synchronized 
            public void save(int money) {
                lock.lock();
                try{
                    account += money;
                }finally{
                    lock.unlock();
                }

            }
        }

注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
4,Atomic
原理:采用CAS+volatile的方式实现,CAS保证了原子性,volatile保证可见性故Atomic实现同步。
CAS:

/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

从注释就可以看出:当线程写数据的时候,先对内存中要操作的数据保留一份旧值,真正写的时候,比较当前的值是否和旧值相同,如果相同,则进行写操作。如果不同,说明在此期间值已经被修改过,则重新尝试。
CAS利用CPU调用底层指令实现,即硬件加锁完成。
两种方式:总线加锁或者缓存加锁保证原子性。
总线加锁
如i=0初始化,多处理器多线程环境下进行i++操作,处理器A和处理器B同时读取i值到各自缓存中,分别进行递增操作,i的值为1。处理器提供LOCK#信号对总线进行加锁后,处理器A读取i的值并递增,此时处理器B被阻塞,无法读取内存中的值。
缓存加锁
总线加锁,在LOCK#信号下,其他线程无法操作内存,性能较差,缓存加锁能较好处理该问题。
缓存加锁,处理器A和B同时读取i值到缓存,处理器A提前完成递增,数据立即回写到主内存,并让处理器B缓存该数据失效,处理器B需重新读取i值。
atomic使用例子:

public class UseAtomic {

   public static void main(String[] args) {
       AtomicInteger atomicInteger=new AtomicInteger();
       for(int i=0;i<10;i++){
            Thread t=new Thread(new AtomicTest(atomicInteger));
            t.start();
            try {
               t.join(0);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

       }
       System.out.println(atomicInteger.get());
   }
}
class AtomicTest implements Runnable{
   AtomicInteger atomicInteger;

   public AtomicTest(AtomicInteger atomicInteger){
       this.atomicInteger=atomicInteger;
   }
   @Override
   public void run() {
       atomicInteger.addAndGet(1);
       atomicInteger.addAndGet(2);
       atomicInteger.addAndGet(3);
       atomicInteger.addAndGet(4);
   }

}

最终的输出结果为100,可见这个程序是线程安全的。如果把AtomicInteger换成变量i的话,那最终结果就不确定了。
这个例子可以作为一道面试题,多个线程同时开启,怎么保证最后结果为100?
参考文章
5.ThreadLocal。
原理:各自线程有各自的ThreadLocalMap成员变量里面放各自线程set进去的变量,故各自线程get的变量是各自的,所以不存在别的线程修改了变量值,所以是同步的。
Thread有变量ThreadLocal.ThreadLocalMap, ThreadLocalMap 里面有Entry 数组,Entry 构造函数如下:

static class Entry extends WeakReference<ThreadLocal> { 
    /** The value associated with this ThreadLocal. */ 
    Object value; 

    Entry(ThreadLocal k, Object v) { 
        super(k); 
        value = v; 
    } 
}

即Entry里面有ThreadLocal和变量值,这样如果set了变量值get取出来是当前线程的变量值。其中这里的ThreadLocal就是所有线程最外边使用的公共变量ThreadLocal如下例中的booleanThreadLocal。
例子如下:

private ThreadLocal<Boolean> booleanThreadLocal = new ThreadLocal<>();
... 

button.setOnClickListener(new View.OnClickListener() { 
    @Override 
    public void onClick(View v) { 
        booleanThreadLocal.set(true); 
        booleanThreadLocal.set(false); 
        Log.i(TAG, "run: ThreadName-------->"+Thread.currentThread().getName()+booleanThreadLocal.get()); 
        new Thread("Thread1"){ 
            @Override 
            public void run() { 
                booleanThreadLocal.set(true); 
                Log.i(TAG, "run: Thread1+ThreadName-------->"+Thread.currentThread().getName()+booleanThreadLocal.get()); 
            } 
        }.start(); 
        new Thread("Thread2"){ 
            @Override 
            public void run() { 
                Log.i(TAG, "run: Thread2+ThreadName-------->"+Thread.currentThread().getName()+booleanThreadLocal.get()); 
            } 
        }.start(); 
    } 
});

打印结果如下:

I/MainActivity: run: ThreadName-------->main---->false 
I/MainActivity: run: Thread2+ThreadName-------->Thread2---->null 
I/MainActivity: run: Thread1+ThreadName-------->Thread1---->true

参考文章
ThreadLocal使用例子:

public class SequenceNumber {
 private static ThreadLocal<Integer> seqNum 
  = new ThreadLocal<Integer>() {
  public Integer initialValue(){
   return 0;
  }
 };
 public int getNextNum(){
  seqNum.set(seqNum.get()+1);
  return seqNum.get();
 }
 public static void main(String[] args){
  SequenceNumber sn = new SequenceNumber();
  TestClient t1 = new TestClient(sn); 
  TestClient t2 = new TestClient(sn);
  TestClient t3 = new TestClient(sn);
  t1.start();
  t2.start();
  t3.start();
 }
 private static class TestClient extends Thread {
  private SequenceNumber sn;
  public TestClient(SequenceNumber sn){
   this.sn = sn;
  }
  public void run(){
   for(int i=0;i<3;i++){
    System.out.println("thread["+Thread.currentThread().getName()
      +"]sn["+sn.getNextNum()+"]");
   }
  }
 }

}

结果如下:
Running results:
thread[Thread-0]sn[1]
thread[Thread-2]sn[1]
thread[Thread-1]sn[1]
thread[Thread-2]sn[2]
thread[Thread-0]sn[2]
thread[Thread-2]sn[3]
thread[Thread-1]sn[2]
thread[Thread-1]sn[3]
thread[Thread-0]sn[3]
三.小知识点
1.线程run方法不会启动新的线程,在调用run方法的线程中运行,而start方法会new出一个新线程,在这个新线程中执行run方法
2.sleep(),wait(),notify()区别:
sleep是Thread的方法,不会释放对象锁,wait(),notify()必须在synchronized里面使用,wait是object的方法,会释放对象锁,notify不会释放对象锁,只是会唤醒wait的线程,让该线程等待状态,此时该线程并没有获取到锁,只有等到当前持有锁的线程即线程的synchronized里面代码运行完成后,其他被唤醒处于等待状态的线程竞争到锁后才能执行
3.volatile使用约束条件:
对变量的写操作不依赖于当前值。(所以什么a++之类的则volatile不能修饰a)
volatile变量不能用于约束条件中:
【反例:volatile变量不能用于约束条件中】 下面是一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。

public class NumberRange {  
    private int lower, upper;  

    public int getLower() { return lower; }  
    public int getUpper() { return upper; }  

    public void setLower(int value) {   
        if (value > upper)   
            throw new IllegalArgumentException(...);  
        lower = value;  
    }  

    public void setUpper(int value) {   
        if (value < lower)   
            throw new IllegalArgumentException(...);  
        upper = value;  
    }  
}  

将 lower 和 upper 字段定义为 volatile 类型不能够充分实现类的线程安全。否则,如果凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,如果初始状态是(0, 5),同一时间内,线程 A 调用setLower(4) 并且线程 B 调用setUpper(3),显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是(4, 3) —— 一个无效值。
4.volatile的适用场景:
模式 #1:状态标志
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

volatile boolean shutdownRequested;  

...  

public void shutdown() {   
    shutdownRequested = true;   
}  

public void doWork() {   
    while (!shutdownRequested) {   
        // do stuff  
    }  
}  

线程1执行doWork()的过程中,可能有另外的线程2调用了shutdown,所以boolean变量必须是volatile。
而如果使用 synchronized 块编写循环要比使用 volatile 状态标志编写麻烦很多。由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile。

这种类型的状态标记的一个公共特性是:通常只有一种状态转换;shutdownRequested 标志从false 转换为true,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察觉的情况下才能扩展(从false 到true,再转换到false)。此外,还需要某些原子状态转换机制,例如原子变量。

模式 #2:一次性安全发布(one-time safe publication)
在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。
这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象。参见:【设计模式】如单例模式(以及多线程、无序写入、volatile对单例的影响)

//注意volatile!!!!!!!!!!!!!!!!!
private volatile static Singleton instace;

public static Singleton getInstance(){
//第一次null检查
if(instance == null){
synchronized(Singleton.class) { //1
//第二次null检查
if(instance == null){ //2
instance = new Singleton();//3
}
}
}
return instance;

如果不用volatile,则因为内存模型允许所谓的“无序写入”,可能导致失败。——某个线程可能会获得一个未完全初始化的实例。
考察上述代码中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 instance 来引用此对象。这行代码的问题是:在Singleton 构造函数体执行之前,变量instance 可能成为非 null 的!
什么?这一说法可能让您始料未及,但事实确实如此。

在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设上述代码执行以下事件序列:
线程 1 进入 getInstance() 方法。
由于 instance 为 null,线程 1 在 //1 处进入synchronized 块。
线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非null。
线程 1 被线程 2 预占。
线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将instance 引用返回,返回一个构造完整但部分初始化了的Singleton 对象。
线程 2 被线程 1 预占。
线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。
更多例子参考:
volatile更多使用点我

5.synchronized和ReetrantLock区别:
a.synchronized代码运行完会自动释放锁,而ReetrantLock一定要求程序员手工释放,并且必须在finally从句中unlock释放否则会死锁;
b.如果在运行加锁代码时发生异常,synchronized会释放锁,ReetrantLock不会释放锁必须在finally中unlock释放
c.synchronized和ReetrantLock默认都是非公平锁,但是ReetrantLock(true)构造函数是公平锁,默认不传即false。
d.并发数少synchronized和ReetrantLock性能差不多,高并发ReetrantLock性能要好一些
另外,高并发 或者锁投票,定时锁等候和中断锁等候等高级使用方法可以选择ReetrantLock。
6.synchronized和ReetrantLock,Atomic性能比较:
synchronized:
在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。
ReentrantLock:
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
Atomic:
和上面的类似,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。
所以,我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。
7.synchronized做同步修饰方法,代码块,静态方法区别,以及括号里面锁传this,class区别
8.CAS优缺点
CAS由于是在硬件层面保证的原子性,不会锁住当前线程,它的效率是很高的。
CAS虽然很高效的实现了原子操作,但是它依然存在三个问题。
a、ABA问题。CAS在操作值的时候检查值是否已经变化,没有变化的情况下才会进行更新。但是如果一个值原来是A,变成B,又变成A,那么CAS进行检查时会认为这个值没有变化,但是实际上却变化了。ABA问题的解决方法是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
b、并发越高,失败的次数会越多,CAS如果长时间不成功,会极大的增加CPU的开销。因此CAS不适合竞争十分频繁的场景。
c、只能保证一个共享变量的原子操作。当对多个共享变量操作时,CAS就无法保证操作的原子性,这时就可以用锁,或者把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
9.死锁例子:
死锁例子

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值