假面骑士exaid
本文分成两个小节:
- 线程的概念 基本使用 线程的状态 和 线程的分类
- Java中的原子操作 以及
volatile&synchronized关键字的基本用法
线程的概念
关于线程的概念,可以参考知乎上的这篇文章,简单来说线程工作在进程上的小“进程”。
线程的四种状态
- 新建
线程对象创建完成,但是尚未分配时间片 - 就绪
分配了时间片,可以运行,但是等待调度 - 阻塞
能够运行,但是有某种情况发生了,进行了阻塞 - 死亡
一般来说就是从run()中返回
线程的基本使用
线程的创建
-
Runnable接口 -
Thread类
Thread类实现了Runnable接口,在Thread的构造函数中传入一个匿名内部类
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setUncaughtExceptionHandler(new MyExceptionHandler());
return thread;
}
-
Callable类
Callable 类是Java5中新加入的,一般和ExecutorService一起使用,在其submit()方法中会返回一个Future对象 。
它和Runnable的区别: - 可以拥有返回值
-
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中不是一个原子操作(为了跨平台性),所以有可能向这篇文中所述,在第一个线程的assign后store前,另外一个线程对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关键字
- 借助这篇博客中的几个例子,来分析一下
synchronized关键字的典型用法
Java 中的synchronized关键字
-
synchronized关键字修饰一个代码块和一个方法:
Demo1和Demo4
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。
由于Demo4中synchronized控制的范围相同,所以结果也是一样的。
- 修饰静态方法和修饰类
如果是静态方法的话,不可能使用控制代码块的写法,修饰静态方法时锁定了类的所有对象,只要一个对象的锁没有解除,其他的也不可以访问。同样的我们借助这篇博客的例子:
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;
}
}
同时有两个线程A,B想获取Singleton,但是A持有锁,所以需要等待A初始化完成之后获取一个初始化完成的单例。 因为JVM为了性能会进行指令出的重排序,正确的顺序应该是:
- 给
uniqueInstance分配内存 - 使用构造函数初始化
uniqueInstance - 将
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 有感 遂记之











网友评论