Java编程思想-并发(1)

线程的基本机制

并发编程使我们可以将程序划分为多个分离、独立运行的程序。一个线程就是在进程中的一个执行路径。在使用线程时,CPU将轮流给每个任务分配其占用时间。每个任务都觉得自己在一直占用CPU,但事实上CPU时间是划分成片段分配给了所有的任务(例外情况是程序却是运行在具有多核CPU的设备上)。而线程的一大好处是可以使你从这个层次抽身出来,即代码不必知道它是运行在具有一个还是多个CPU的机器上。多任务和多线程往往是使用多处理器系统的最合理的方式。

定义任务

线程可以驱动任务,可以用Runnable接口、实现run方法的方式来描述任务,如,下面的LiftOff就是描述了一个任务:

public class LiftOff implements Runnable {
    protected int countDown = 10;
    private static int taskCount = 0;
    private final int id = taskCount++;
    public LiftOff() {

    }
    public LiftOff(int countDown) {
        this.countDown = countDown;
    }
    public String status() {
        return "#" + id + "(" + (countDown > 0 : "LiftOff!") + ")" + "), ";

    }
    public void run() {
        while(countDown-- > 0) {
            System.out.println(status());
            Thread.yield();

        }

    }

}

任务的run方法通常总会有某种形式的循环,使得任务一直运行下去直到不再需要,所以要设定跳出循环的条件。通常run方法被写成无限循环的形式。除非主动终止,否则run方法将永远运行下去。

静态方法Thread.yield()表示对调用线程调度器(可以将CPU从一个线程转移给另一线程)的一种建议。

下面来执行上述任务,事实上,这是直接在主线程中执行的该任务,相当于方法调用:

public class MainThread {
    public static void main(String[] args) {
        LiftOff launch = new LiftOff();
        launch.run();
    }
}

输出:

#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(LiftOff!),

上面的例子表明,run方法本省不具有任何特殊之处——他不存在产生任何内在的线程能力。要实现线程行为,你必须显式地调用Thread.start()方法启动。

Thread类

将Runnable对象转变为工作任务的传统方式是提交给一个Thread构造器,并显式调用Thread的start方法来启动一个新的线程:

public class BasicThreads {
    public static void main(String[] args) {
        Thread t = new Thread(new LiftOff());
        t.start();
        System.out.println("Waiting for LiftOff!");

    }
}

输出结果:

Waiting for LiftOff!
#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(LiftOff!),

每次执行的结果可能不一样。

Thread的构造器需要一个Runnable对象。从执行结果来看,start方法迅速返回了。实际上,start方法的作用是在一个新的线程中启动对run方法的调用。由于run方法是在新的线程中执行的,所以,main方法中的代码仍可以正常执行,也就是说,main方法和LiftOff.run方法时程序中与其他线程“同时”执行的代码。

启动多个线程,“同时”执行多个任务:

public class MoreBasicThreads {
    public static void main(String[] args) {
        for(int i = 0; i < 5; ++i) {
            new Thread(new LiftOff()).start();
        }
        System.out.println("Waiting for LiftOff!");
    }
}
//输出结果
Waiting for LiftOff!
#0(9),#1(9),#2(9),#3(9),#4(9), #0(8), #1(8), #2(8), #3(8), #4(8), #0(7), #1(7), #2(7),#3(7),#4(7),#0(6),#1(6),#2(6),#3(6),#4(6), #0(5), #1(5), #2(5), #3(5), #4(5), #0(4),#1(4),#2(4),#3(4),#4(4), #0(3), #1(3), #2(3), #3(3), #4(3), #0(2), #1(2), #2(2),#3(2),#4(2),#0(1),#1(1),#2(1),#3(1),#4(1), #0(0), #1(0), #2(0), #3(0), #4(0), #0(LiftOff!),#1(LiftOff!),#2(LiftOff!),#3(LiftOff!),#4(LiftOff!),


说明不同的线程在运行时序上交织在了一起,这就是并发执行。

这个程序在多次运行时,可能会产生不同的结果。因为线程调度器并不由程序员控制,它是非确定性的。


使用Executor(线程池)

JDK1.5的java.util.concurrent包中的Executor可以自动管理Thread对象。从而简化并发编程。

下面的类为每个任务都创建了一个线程。

public class ChchedThreadPool {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i = 0;i < 5; ++i) {
            exec.execute(new LiftOff());
        }
        exec.shutdown();
    }
}
//输出结果
Waiting for LiftOff!
#0(9),#1(9),#2(9),#3(9),#4(9), #0(8), #1(8), #2(8), #3(8), #4(8), #0(7), #1(7), #2(7),#3(7),#4(7),#0(6),#1(6),#2(6),#3(6),#4(6), #0(5), #1(5), #2(5), #3(5), #4(5), #0(4),#1(4),#2(4),#3(4),#4(4), #0(3), #1(3), #2(3), #3(3), #4(3), #0(2), #1(2), #2(2),#3(2),#4(2),#0(1),#1(1),#2(1),#3(1),#4(1), #0(0), #1(0), #2(0), #3(0), #4(0), #0(LiftOff!),#1(LiftOff!),#2(LiftOff!),#3(LiftOff!),#4(LiftOff!),


单个的Executor通常被用来创建和管理系统中的所有任务。

调用shutdown方法可以防止新任务被提交给这个Executor,当前线程(即主线程)将继续运行在shutdown被调用之前提交的所有任务。而这个程序将在所有这些任务执行完毕后尽快推出。

除了newCachedThreadPool外,还有不同类型的Executor,如newFixedThreadPool, 它使用了有限的线程集来执行提交的任务:

public class FixeedThreadPool {
    public static void main(String[] args) {
        ExecutorService exec = Executors.new FixedThreadPool(5);
        for(int i = 0;i < 5;++i) {
            exec.execute(new LiftOff());
        }
        exec.shutdown();
    }
}
//输出结果
Waiting for LiftOff!
#0(9),#1(9),#2(9),#3(9),#4(9), #0(8), #1(8), #2(8), #3(8), #4(8), #0(7), #1(7), #2(7),#3(7),#4(7),#0(6),#1(6),#2(6),#3(6),#4(6), #0(5), #1(5), #2(5), #3(5), #4(5), #0(4),#1(4),#2(4),#3(4),#4(4), #0(3), #1(3), #2(3), #3(3), #4(3), #0(2), #1(2), #2(2),#3(2),#4(2),#0(1),#1(1),#2(1),#3(1),#4(1), #0(0), #1(0), #2(0), #3(0), #4(0), #0(LiftOff!),#1(LiftOff!),#2(LiftOff!),#3(LiftOff!),#4(LiftOff!),


newFixedThreadPool一次性的预先执行了代价高昂的线程分配,如果任务数多于线程数,多出的任务将会排队,如果任务数小于线程数,空线的线程也不会销毁。

SingleThreadExecutor是线程数为的newFixedThreadPool,所有的任务都会运行在一个线程中,所以这些任务是串行执行,并且执行的顺序就是提交任务的顺序:

//只将上面的代码改为newSingleThreadPool,其他不变
//输出结果:
#0(9),#0(8), #0(7),#0(6), #0(5),#0(4),#0(3),#0(2),#0(1),#0(0),#0(LiftOff!),#1(9),#1(8),  #1(7), #1(6), #1(5), #1(4), #1(3),#1(2), #1(1), #1(0),#1(LiftOff!),#2(9) ... ... #4(0), #4(LiftOff!),

可以看到,输出时以串行的方式输出的。

从任务中产生返回值-Future和Callable

Runnable可以在新的线程中执行任务,但是无法返回任何值。而实现Callable接口可以实现“当任务完后能返回一个值”的诉求。Callable从JDK1.5引入,它包含一个泛型参数,它表示从Callable接口的call方法中返回的值。与run方法,call方法不具备任何线程的能力,要想使call方法运行在一个单独的线程中,必须调用ExecutorService.submit()方法:

class TaskWithResult implements Callable<String> {
    private int id;
    public TaskWithResult(int id) {
        this.id = id;
    }
    @Override
    public String call() {
        return "result of TaskWithResult " + id;

    }

}

public class  CallableDemo {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool():
        ArrayList<Future<String>> results = new ArrayList<>();
        for(int i = 0; i < 10;++i) {
            results.add(exec.submit(new TaskWithResult(i)));
        }
        for(Future<String> fs : result) {
            try {
                System.out.println(fs.get());
            } catch(InterruptedException e) {
                System.out.print(e);
                return ;
            }
             catch(ExecutionExeception e) {
                System.out.print(e);
                return ;
            } finally {
                exec.shutdown();
            }
        }
    }
}
//输出结果:

result of TaskWithResult 0
result of TaskWithResult 1
result of TaskWithResult 2
result of TaskWithResult 3
result of TaskWithResult 4
result of TaskWithResult 5
result of TaskWithResult 6
result of TaskWithResult 7
result of TaskWithResult 8
result of TaskWithResult 9

休眠 sleep、优先级、让步 yield、加入一个线程join

可以参考这篇文章:线程协作 生产者/消费者、线程中断、线程让步、线程睡眠、线程合并


后台线程 (Deamon Thread)

后台(deamon)线程与普通线程的区别是,前者并不是不可或缺的——当所有的非后台线程结束时,程序也就终止了,这时所有的后台线程也将终止。但是只要有非后台线程在运行,程序就不会终止:

public class SimpleDeamons implements Runable {
    @Override
    public void run() {
        try {
            while(true) {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread() + " " + this);
            }
        } catch (InterruptedException e) {
            System.out.println("sleep() imterrupted");
        }
    }
    public static void main(String[] args) {
        for(int i = 0;i < 10; ++i) {
            Thread daemon = new Thread(new SimpleDaemon());
            //该设置必须在start之前调用
            daemon.setDeamon(true);
        }
        System.out.println("All daemons started");
        TimeUnit.MILLISECOND.sleep(125);
    }

}
//输出结果
All daemons started
Thread[Thread-0,5,main] SompleDaemon@5332ad
Thread[Thread-1,5,main] SompleDaemon@6adfc3
Thread[Thread-2,5,main] SompleDaemon@947abc
Thread[Thread-3,5,main] SompleDaemon@16caf4
Thread[Thread-4,5,main] SompleDaemon@1d58aa
Thread[Thread-5,5,main] SompleDaemon@84cc67

从结果可以看出,后台线程只执行了7个,这是因为此时主线程已经结束了,这时程序中已经没有了除后台线程外的其他线程,所以后台线程也立刻结束。

可以通过调用isDaemon方法来判断某线程是否是后台线程,如果是一个后台线程,那么它创建的任何线程都将被自动设置成后台线程。

class Daemon implements Runnable {
    private Thread[] t = new Thread[10];
    public void run() {
        for(int i = 0;i < t.length;++i) {
            t[i] = new Thread(new DaemonSpawn());
            t[i].start();
            System.out.print("DaemonSpawn " + " started, ");

        }
        for(int i = 0;i < t.length;++i) {
            print("t[" + i + "].isDaemon() = " + t[i].isDaemon() + ", ");
        }
        while(true) {
            Thread.yield():
        }
    }
}

class DaemonSpawn implements Runnable {
    public void run() {
        while(true) {
            Thread.yield();
        }
    }
}

public class Daemons {
    public static void main(String[] args) throws Exception {
        Thread d = new Thread(new Daemon());
        d.setDaemon(true);
        d.start();
        System.out.print("d.isDaemon() = " + d.isDaemon() + ", ");
        TimeUnit.SECONDS.sleep(1);

    }
}
//输出结果:

d.isDaemon() = true, DaemonSpawn 0 started,DaemonSpawn 1 started,DaemonSpawn 2 started,DaemonSpawn 3 started,DaemonSpawn 4 started,DaemonSpawn 5 started,DaemonSpawn 6 started,DaemonSpawn 7 started,DaemonSpawn 8 started,DaemonSpawn 9 started, t[0].isDaemon() = true,t[1].isDaemon() = true,t[2].isDaemon() = true,t[3].isDaemon() = true,t[4].isDaemon() = true,t[5].isDaemon() = true,t[6].isDaemon() = true,t[7].isDaemon() = true,t[8].isDaemon() = true,t[9].isDaemon() = true,

从输出结果可以看得出来,由一个后台线程启动的10个线程都默认是后台线程。

关于后台线程,还有一点要说明的是,即便是为后台线程编写了finally块,在所有非后台线程结束的时候,如果后台程序没有运行完,也不会执行finally块,后台线程立即终止:

class ADmaeon implements Runnable {
    public void run() {
        try {
            System.out.print("Starting ADaemon");
            TimeUnit.SECONDS.sleep(1);
        } catch(InterruptedException e) {
            System.out.print("Exiting");
        } finally {
            System.out.print("run?");
        }
    }
}
public class DaemonsDontRunFinally {
    public static void main(String[] args) throwsException {
        Thread t = new Thread(new ADaemon());
        t.setDaemon(true);
        t.start();
    }
}
//输出
Starting ADaemon

如果注释掉t.setDaemon(true);这句代码,finally子句将得到执行。

共享受限资源

当某个访问某项资源的线程上加锁,可以使该线程独占资源,在任务被解锁前,其他任务将无法访问该资源。

Java提供关键字synchronized像是,为防止资源冲突提供了内置的支持。当任务要执行被synchronized关键字保护的代码片段的时候,他将检查锁是否可用,然后获取锁,执行代码,释放锁。

要控制对共享资源的访问,得先把它包装进一个对象。然后把所有要访问这个资源的方法标记为synchronized。如果某个人物处于一个对标记为synchronized的方法的调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized放的线程都将被阻塞。(注意:在static方法上标记synchronized和非static方法上标记synchronized,它们使用的并不是一把锁。

所有对象都自动含有单一的锁。当在对象上调用其任意synchronized方法的时候,此对象都被加锁。这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。也就是说,对于某个特定对象来说,其所有synchronized方法共享同一个锁。

Java中的synchronized锁机制是可重入的,即一个任务可以多次获得对象的锁。JVM负责跟踪对象加锁的次数,如果一个对象被解锁(锁被完全释放),计数器变为0。在任务第一次给对象加锁的时候,计数器变为1。每当这个相同的任务在这个对象上获得锁的时候,计数器就会+1。显然,只有首先获得了锁的时候,计数的任务才能允许继续获得锁。每当任务离开一个synchronized方法,计数递减,当计数器为零的时候,锁被完全释放,此时别的任务就可以使用此资源。

JDK1.5中提供了Lock类,用于显示地创建、释放、锁定。因此,它与synchronized相比,代码量多,但是更加灵活:使用synchronized时,如果某些事物失败了,那么就会跑出一个异常,但是你没有机会做任何清理工作,已维护系统使其处于良好状态。Lock对象可以在finally中将系统维护在正确的状态;用synchronized关键字不能尝试着获取锁且最终获取会失败(synchronized是一种悲观锁),或者尝试着获取一段时间,然后放弃它,但是Lock可以做到:

public class AttemptingLocking {
    private ReentrantLock lock = new ReentrantLock();
    public void untimed() {
        boolean captured = lock.tryLock();
        try {
            System.out.println("tryLock() : " + captured);
        } finally {
            if(captured) {
                lock.unlock();
            }
        }
    }
    public void timed() {
        boolean captured = false;
        try {
            captured = lock.tryLock(2, TimeUnit.SECONDS);

        } catch(InterrputedException e) {
            throw new RuntimeException(e);

        }
        try {
            System.out.println("tryLock(2, TimeUnit.SECONDS): " + captured);
        } finally {
            if(captures) {
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        final AttemptLocking al = new AttemptLocking();
        al.untimed();
        al.timed();
        new Thread() {
            {setDaemon(true);}
            public void run() {
                al.lock.lock();
                System.out.println("acquired");
            }
        }.start();
        Thread.yield();
        al.untimed();
        al.timed();

    }
}
//输出
tryLock() : true
tryLock(2,TimeUnit.SECONDS) : true
acquired
tryLock() : false
tryLock(2, TimeUnit.SECONDS) : false

在主线程中,程序首先获得了lock,然后被释放,接着,主线程尝试在两秒中内不断获取锁,由于该lock锁没有被占用,所以立即返回true,接着释放锁;接着在开启一个守护线程,并获得lock,同时在主线程中调用untimed尝试立即获取lock,由于该lock被守护线程占用,所以无法获取,接着调用timed方法,表示希望在两秒内获取lock,如果没获取到就不再尝试获取了,结果显然没获取到,于是返回false。这种方式不像synchronized一样,synchronized是如果锁被占用,那么其他线程将一直阻塞,直到该锁被释放,但是ReentrantLock可以允许你尝试获取一段时间,如果没获取到就不等了,去执行一些其他事情。

原子性与可见性

原子性说的是,在某段代码的执行过程中,不能进行线程的之间的切换,那么这段代码就是原子性的。在基本类型变量中,除了long和double,都是具有读写原子性,即,除了long和double,其他基本类型变量在多线程的环境下,某一个线程读(或者写)该变量的过程中,是不能切换线程的。但是由于long和double是64位的,JVM按照每次32位读取或写入一个变量,所以long和double的读或写是分为两步的,那么这就不是原子的,这被称为字撕裂。

解决方法是为long或double域加上一个volatile关键字。被修饰为volatile的域可以保证多线程环境下,对该域的基本操作(读或者写)是原子的,不会对该变量进行基本操作时切换线程。

可见性说的是,当一个线程修改了某个域时,另一个线程可能没法立刻获取该刚刚被那个线程修改的值,而只能获取到修改之前的值。这是JVM为提高并发程序的运行效率的一种优化。因为我们知道,非静态域是初始化在堆内存中的,而多线程在访问堆内存的域时,每一个线程实际上是先把堆内存的这个变量复制一份到自己的一个私有域中,再进行操作最后把计算的值刷新到堆内存中,而这个私有域是在CPU的寄存器中的。为啥要这么做呢,就是因为堆内存的读写速度和CPU的寄存器的读写速度是有一个数量级的差距的,如果每个线程都直接读写堆内存的变量,效率会大大降低。但是volatile是不能解决域的可见性的问题的,解决的方式就是使用同步,同步同时保证了域的可见性和原子性。而volatile只保证了原子性:

public class AtomicityTest implements Runnable {
    private int i = 0;
    public int getValue() {
        return i;
    }
    private synchronized void eventIncrement() {
        ++i;
        ++i;
    }
    public void run {
        while(true) {
            eventIncrement();
        }
    }
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicityTest at = new AtomicityTest();
        exec.excute(at);
        while(true) {
            int val = at.getValue();
            if(val % 2 != 0) {
                System.out.println(val);
                System.exit(0);

            }
        }
    }
}
//输出
191583767

主线程不断判断子线程值i的奇偶性,如果i值为奇数就退出程序,而子线程每次都为i自增2(初始值为0),所正常情况下,程序应该永远不会退出。但是程序就偏偏退出了,而且打印了一个奇数。

有两个原子到时结果出错:1、调用getValue方法时,并没有同步;
2、i值没有保证可见性。
解决方式1:给getValue方法加上synchronized关键字,保证getValue和eventIncrement共享同一把锁。

解决方式2:将i声明为AtomicInteger类型的。(在下面介绍)
注意,不能只使用volatile修饰i,这样无法解决问题,因为volatile不能使域具备可见性,而synchronized可以同时保证可见性和原子性,但是必须保证在所有使用到该域的地方同步。

原子类

JDK1.5中引入了原子类,包括AtomicInteger,AtomicLong,AotimicReference等。这些类保证了并发时的原子性和可见性,同时是线程安全的:

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() {
        evenIncrement();
    }
    public static void main(String[] args) {
        new Timer().schedule(new TimerTask(){
            public void run() {
                System.err.println("aborting");
                System.exit(0);

            }
        },5000);
        ExecutorService exec = Excutors.newCachedThreadPoo();
        AtomicIntegerTest ait = new AtomicIntegerTest();
        exec.excute(ait);
        while(true) {
            int val = ait.getValue();
            if(val % 2 != 0) {
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

程序中没有加入任何synchronized或是volatile等关键字,但将正确运行,所以使用TimerTask将它在5s后关闭。

需要说明的是,Atomic类应该只在特殊情况下才使用它们,通常应该依赖于synchronized、Lock对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值