美文网首页
Java 并发(1)-- 线程的基本概念 以及 volatile

Java 并发(1)-- 线程的基本概念 以及 volatile

作者: kolibreath | 来源:发表于2020-02-29 19:41 被阅读0次
假面骑士exaid

本文分成两个小节:

  1. 线程的概念 基本使用 线程的状态 和 线程的分类
  2. Java中的原子操作 以及 volatile & synchronized关键字的基本用法

线程的概念

关于线程的概念,可以参考知乎上的这篇文章,简单来说线程工作在进程上的小“进程”。

线程的四种状态

  • 新建
    线程对象创建完成,但是尚未分配时间片
  • 就绪
    分配了时间片,可以运行,但是等待调度
  • 阻塞
    能够运行,但是有某种情况发生了,进行了阻塞
  • 死亡
    一般来说就是从run()中返回

线程的基本使用

线程的创建

  1. Runnable接口
  2. Thread
    Thread类实现了Runnable接口,在Thread的构造函数中传入一个匿名内部类
 @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setUncaughtExceptionHandler(new MyExceptionHandler());
        return thread;
    }
  1. Callable
    Callable 类是Java5中新加入的,一般和ExecutorService一起使用,在其submit()方法中会返回一个Future对象 。
    它和Runnable的区别:
  2. 可以拥有返回值
  3. get()方法会造成阻塞
    下面是一个使用Callable计算斐波拉契数列的例子
import java.util.concurrent.*;

public class FibonaciCallable implements Callable<Integer> {

    private int fn;
    private int fnMinus1 = 1;
    private int fnMinus2 = 1;

    private int total;

    public FibonaciCallable(int total){
        this.total = total;
    }

    @Override
    public Integer call() throws Exception {
        for (int i = 1; i < total; i++) {
            fn = fnMinus1 + fnMinus2;
            fnMinus2 = fnMinus1;
            fnMinus1 = fn;
        }
        System.out.println("我计算完成!");
        TimeUnit.SECONDS.sleep(3);
        return fn;
    }

    public static void main(String args[]){
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<Integer> result = executorService.submit(new FibonaciCallable(4));
        System.out.println("我在等待结果!");
        try {
            //产生阻塞 一直到任务完成时才会返回结果
            System.out.println(result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
           executorService.shutdown();
        }
    }
}

打印结果

我在等待结果!(立刻出现)
我计算完成!    (立刻出现)
5  (等待了数秒之后出现)

线程的使用

这个地方主要说明一下Thread类中提供的两个静态方法

  • Thread.sleep()
    让当前线程休眠,不过现在推荐使用TimeUnit.SECONDS.sleep();方法
  • Thread.yield()
    让当前线程让步,使当前线程从执行状态(运行状态)变为可执行态(就绪状态),这不代表着这个线程在下次的时候不会被调度器选择执行。可以看看这个例子:
public class LiftOff implements  Runnable{

    private int countDown;
    private static int taskCount = 0;
    private final int id = taskCount;

    public LiftOff(int countDown){
        taskCount++;
        this.countDown = countDown;
    }

    public void reset(int cd){
        countDown = cd;
    }

    @Override
    public void run() {
        while(countDown-->0){
            System.out.println("# id= "+id +" "+this.countDown);
            Thread.yield();
        }
        System.out.println("lift off!");
    }

    public static void main(String args[]){
        //直接使用LiftOff 实现Runnable
        LiftOff liftOff = new LiftOff(10);
        liftOff.run();

        
        //使用Thread 类调用Runnable作为参数
        LiftOff liftOff2 = new LiftOff(10);
        Thread thread = new Thread(liftOff2);
        thread.start();
        //如果交换两个会发生什么?
    }
}


这个例子模拟一个火箭升空的行为,其中liftOff运行在主线程(Main),但是liftOff2运行在新开的一个线程。运行结果为:

# id= 0 9
# id= 0 8
# id= 0 7
# id= 0 6
# id= 0 5
# id= 0 4
# id= 0 3
# id= 0 2
# id= 0 1
# id= 0 0
lift off!
# id= 1 9
# id= 1 8
# id= 1 7
# id= 1 6
# id= 1 5
# id= 1 4
# id= 1 3
# id= 1 2
# id= 1 1
# id= 1 0
lift off!

分析运行结果,首先在main线程上创建liftOff,先完成liftOff的倒计时,这个时候在liftOff中即使调用了yield()也没办法,没有其他的线程可以被调度器选择,只得等待这个线程完成倒计时。然后再是liftOff2完成倒计时,如果改变两个liftOff的创建顺序会怎么样?

 //使用Thread 类调用Runnable作为参数
        LiftOff liftOff2 = new LiftOff(10);
        Thread thread = new Thread(liftOff2);
        thread.start();


        //直接使用LiftOff 实现Runnable
        LiftOff liftOff = new LiftOff(10);
        liftOff.run();

首先会创建liftOff2,然后开始其运行,这个时候主线程没有被阻塞,可以创建liftOff。一旦liftOff创建好了,就可以作为被调度的线程,当liftOff2
yield时,有可能被选择上,所以结果大致上为两种id输出交错的样子:

# id= 1 9
# id= 0 9
# id= 1 8
# id= 0 8
# id= 1 7
# id= 0 7
# id= 1 6
# id= 0 6
# id= 1 5
# id= 0 5
# id= 1 4
# id= 0 4
# id= 1 3
# id= 0 3
# id= 1 2
# id= 0 2
# id= 1 1
# id= 0 1
# id= 1 0
# id= 0 0
lift off!
lift off!

Java中的原子操作和互斥

Java中线程内存模型

内存模型

什么是原子操作

Java中定义了8种工作内存和主内存的原子操作:详见这篇文章
设想这样的一种情况,两个线程同时操作一个共有的变量a,都想给a++,正如-这篇文章所描述的。因为a++在Java中不是一个原子操作(为了跨平台性),所以有可能向这篇文中所述,在第一个线程的assignstore前,另外一个线程对a完成+1,所以最后a并没有变成2。为了线程中的变量可见性,我们引入volatile关键字

Java 中的volatile关键字

volatile关键字是Java的一种弱同步机制,在线程中没有进行缓存和重排序,并且volatile不能保证原子性:下面我们看一个例子:

public class VolatileDemo {

    private static CountDownLatch latch = new CountDownLatch(100000000);

    private static int value = 0;
    public static class Task implements Runnable{

        private int id;
        public Task(int id){
            this.id = id;
        }

        public void run(){
            value++;
            System.out.println("#id " + id + " value is "+ value);
            latch.countDown();
        }
    }

    public static void main(String args[]) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for(int i = 0 ;i< 100000000; i ++){
            executorService.execute(new Task(i));
        }
        try {
            latch.await();
            System.out.println("finished");
            executorService.shutdownNow();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

Task在每次执行的时候将value的值自增,CountDownLatch设置了一个倒计时10000000,正常情况下会结束,但是实际运行过程中由于计时器不会停止,所以不能够结束。这是因为自增操作不是一个原子操作,volatile不能保证原子操作所引起的

除了这一点之外,我们来看一个例子:

IntGenerator 一个抽象类

public abstract class IntGenerator {
    private volatile boolean canceled = false;
    public abstract int next();

    public void cancel(){ canceled = true;}

    public boolean isCanceled(){ return canceled;};
}

对于这个Generator的具象化:EvenGenerator,只生成偶数:

public class EvenGenerator extends IntGenerator {

    private int currentEvenValue = 0;

    @Override
    public int next() {
        ++currentEvenValue;//后面的任务有可能进行错误的计算
        ++currentEvenValue;
        return currentEvenValue;
    }

    public static void main(String args[]) {
      EvenChecker.test(new EvenGenerator());
}

EvenChecker.java

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class EvenChecker implements Runnable {

    private IntGenerator generator;
    private final int id;

    public EvenChecker(IntGenerator g, int id){
        this.id = id;
        this.generator = g;
    }

    @Override
    public void run() {
        //return to notify executorService it's done
        while(!generator.isCanceled()){
            int val = generator.next();
            if(val % 2 !=0){
                System.out.println("#id = " + id + " value = "+ val + " not even!");
                generator.cancel();
            }
        }
    }

    public static void test(IntGenerator generator, int count){
        System.out.println("Press Control-C to exit");
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < count ; i++) {
            executorService.execute(new EvenChecker(generator, i));
        }
        executorService.shutdown();
    }

    public static void test(IntGenerator gp){
        test(gp, 5);
    }
}

我们预期的结果是一个死循环,不会结束的,但是事实并不是这样:

Press Control-C to exit
#id = 0 value = 567 not even!
#id = 1 value = 565 not even!
#id = 2 value = 569 not even!

Process finished with exit code 0

这说明持有同样的generator的对象的不同线程对于currentValue的修改并不是偶数次,然而我们进行控制的cancel却能够在线程中传递, 没有犯错的线程也被取消了。在对这个例子进行修改之前我们先看看synchronized关键字的使用

Java 中的synchronized关键字

  1. synchronized关键字修饰一个代码块和一个方法:
    Demo1Demo4

Demo1

class SyncThread implements Runnable {
   private static int count;

   public SyncThread() {
      count = 0;
   }

   public  void run() {
      synchronized(this) {
         for (int i = 0; i < 5; i++) {
            try {
               System.out.println(Thread.currentThread().getName() + ":" + (count++));
               Thread.sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }

   public int getCount() {
      return count;
   }
}

Demo4

public synchronized void run() {
   for (int i = 0; i < 5; i ++) {
      try {
         System.out.println(Thread.currentThread().getName() + ":" + (count++));
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
}

SyncThread类中的静态变量count随着类的初始化而初始化,并不随着变量的两次初始化

SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();

所以两个线程同时触发,两个线程共用了一个syncThread对象,首先thread1先执行,然后线程睡了0.1s,SyncThread2线程执行没有停止,但是想访问run()代码块中的同步代码块的时候被阻塞。所以需要等到thread1睡醒,然而thread1睡醒,for代码需要继续执行。所以最终的结果是thread1 增加5次,之后才是thread2增到10。

由于Demo4synchronized控制的范围相同,所以结果也是一样的。

  1. 修饰静态方法和修饰类
    如果是静态方法的话,不可能使用控制代码块的写法,修饰静态方法时锁定了类的所有对象,只要一个对象的锁没有解除,其他的也不可以访问。同样的我们借助这篇博客的例子:
    Demo5
class SyncThread implements Runnable {
   private static int count;

   public SyncThread() {
      count = 0;
   }

   public synchronized static void method() {
      for (int i = 0; i < 5; i ++) {
         try {
            System.out.println(Thread.currentThread().getName() + ":" + (count++));
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }

   public synchronized void run() {
      method();
   }
}

调用方法:

SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();

虽然是两个对象,但是和Demo1的结果相同。使用synchronized修饰类的结果和修饰静态方法也是相同的。

Brain的同步规则:
如果你正在写一个变量,这个变量接下来将被另外一个线程读取,或者正在读取上一次被另外一个线程写过的变量,这个时候,你必须使用同步,并且所有读写线程都必须使用相同的监视器锁同步。

使用对类加锁实现单例模式

public class Singleton {
    private volatile static Singleton uniqueInstance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(uniqueInstance == null){
//B发现单例为空 但是A持有锁
            synchronized(Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();            
                }
            }
        }
        return uniqueInstance;
    }
}

同时有两个线程AB想获取Singleton,但是A持有锁,所以需要等待A初始化完成之后获取一个初始化完成的单例。 因为JVM为了性能会进行指令出的重排序,正确的顺序应该是:

  1. uniqueInstance 分配内存
  2. 使用构造函数初始化uniqueInstance
  3. uniqueInstance指向分配的内存空间

    ,在重排序过程中,顺序可能会变成1->3->2,这时候B获取到分配了内存空间的单例,但是初始化却没有完成。

最后再来解决上面的问题

public class SynchronizedEvenChecker extends IntGenerator{

    private int currentEvenValue = 0;
    //父类中的方法是同步方法,子类中的方法不一定会继承
    @Override
    public synchronized int next() {
        currentEvenValue++;
        //甚至可以加入Thread.yield(),让这个线程让步 我们再来观察会不会发生奇怪的情况!
        currentEvenValue++;
        return currentEvenValue;
    }
}

我们给方法加一个同步锁就解决了,但是这并不是唯一的方法,使用加显式锁和加原子类的方法下篇文章再续!
参考内容:

读Thinking In Java 有感 遂记之

相关文章

网友评论

      本文标题:Java 并发(1)-- 线程的基本概念 以及 volatile

      本文链接:https://www.haomeiwen.com/subject/yhppixtx.html