文章目录
Java进阶(二)并发(上篇)
并发设计原理
基本概念
1.并发与并行
并发:在单个处理器上采用单核执行多个任务。
并行:同一时间在不同计算机、处理器或者处理器核心上同时运行多个任务。
2.同步
同步:一种协调两个或更多任务以获得预期结果的机制
同步机制:信号量、监视器
线程安全:如果共享数据的所有用户都受到同步机制的保护,就是线程安全的
3.任务间通信
共享内存、消息传递
并发中的问题
1.数据竞争
2.死锁
当两个或多个任务正在等待必须由另一线程释放的某个共享资源,而该线程又正在等待必须由前述任务释放的另一共享资源时,就发生了死锁。
死锁的四个必要条件:互斥、占有并等待条件、不可剥夺、循环等待
避免死锁的机制:忽略、检测、预防、规避
活锁:两个任务总是因对方的行为而改变自己的状态
并发设计模式
设计模式是针对某一类共同问题的解决方案。这种解决方案被多次使用,而且已经被证明是针对该类问题的最优解决方案。每当你需要解决这其中的某个问题,就可以使用它们来避免做重复工作。
具体的设计模式我们在接下来的章节中边学习边触及
Thread和Runnable
在Java中创建线程有两种方式:
1.拓展Thread类并重载run()方法
2.实现Runnable接口,并将该类的对象传递给Thread类的构造函数
一般使用第二种方式,其拥有更大的灵活性
栗子:
我们设计如下线程,该线程有一个倒计时后发射的功能
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 ? countDown:"LiftOff")+",";
}
public void run(){
while (countDown>=0) {
System.out.print(status());
countDown--;
Thread.yield();
}
System.out.println();
}
}
其中Thread.yield()的作用是对线程调度器的一种建议,表示此时可以将cpu切换到其他线程。
我们看一下运行结果:
public class test {
public static void main(String[] args) {
for(int i=0;i<5;i++) {
new Thread(new LiftOff()).start();
}
}
}/*
#1_10,#1_9,#1_8,#1_7,#2_10,#2_9,#2_8,#2_7,#3_10,#0_10,#3_9,#2_6,#1_6,#2_5,#4_10,#3_8,#0_9,#3_7,#4_9,#2_4,#1_5,#2_3,#4_8,#3_6,#0_8,#3_5,#4_7,#2_2,#1_4,#2_1,#4_6,#4_5,#3_4,#0_7,#3_3,#4_4,#2_LiftOff,#1_3,
#4_3,#3_2,#0_6,#0_5,#0_4,#0_3,#3_1,#4_2,#1_2,#4_1,#3_LiftOff,
#0_2,#4_LiftOff,
#1_1,#0_1,#1_LiftOff,#0_LiftOff,
*/
可以看到线程的切换是毫无规律的。
管理大量线程:执行器
Executor使用
Java5引入了执行器(Executor)为你管理Thread对象,可以在很大程度上简化并发编程。
直接看一个栗子:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorTest {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i=0;i<5;i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}/*
#0_10,#4_10,#2_10,#1_10,#3_10,#1_9,#2_9,#2_8,#2_7,#4_9,#0_9,#4_8,#4_7,#2_6,#1_8,#3_9,#1_7,#2_5,#4_6,#0_8,#4_5,#4_4,#2_4,#1_6,#3_8,#1_5,#2_3,#4_3,#0_7,#0_6,#0_5,#4_2,#2_2,#1_4,#3_7,#1_3,#2_1,#4_1,#0_4,#0_3,#0_2,#0_1,#0_LiftOff,#4_LiftOff,
#2_LiftOff,
#1_2,#3_6,#1_1,
#1_LiftOff,#3_5,
#3_4,#3_3,#3_2,#3_1,#3_LiftOff,
*/
我们得到了类似的结果,其中,CachedThreadPool是一种线程池,其为每个任务都创建一个线程,除了该线程池外,还有FixedThreadPool可以指定参数限制线程的数量,SingleThreadExecutor能使每个任务按顺序进行(即单线程)。
返回值
如果需要每个线程返程后得到一个返回值,则可以实现Callable接口:
public class TaskWithResult implements Callable {
private int id;
public TaskWithResult(int id) {
this.id = id;
}
public String call() throws Exception{
return "return "+id;
}
}
//
public class ExecutorTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
ArrayList<Future<String>> results = new ArrayList<Future<String>>();
for(int i=0;i<5;i++) {
results.add(exec.submit(new TaskWithResult(i)));
}
for(Future<String> fs:results) {
try {
System.out.println(fs.get());
} catch (InterruptedException e) {
System.out.println(e);
} catch (ExecutionException e) {
System.out.println(e);
} finally {
exec.shutdown();
}
}
}
}/*
return 0
return 1
return 2
return 3
return 4
*/
可以看到,Callable接口需要实现Call()方法,并且通过ExecutorService.submit()调用,该方法产生Future对象。我们可以使用isDone()方法查询Future对象是否完成,并用get()方法得到返回结果。
控制任务调度
1.调用sleep()让线程进入睡眠是影响任务行为的一种简单方法,在Java SE5之后的版本,通常使用Time.Unit类来进行睡眠。
2.同时,我们也可以采用优先级来将线程的重要性传递给调度器
public class LiftOff_sleep extends LiftOff{
private int pri;
public LiftOff_sleep(int priority) {
this.pri = priority;
}
@Override
public void run() {
Thread.currentThread().setPriority(pri);
while (countDown>=0) {
System.out.print(status());
countDown--;
}
System.out.println();
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i=0;i<5;i++)
exec.execute(new LiftOff_sleep((1+i)*2));
exec.shutdown();
}
}
/*
#2_10,#2_9,#0_10,#1_10,#4_10,#3_10,#4_9,#4_8,#1_9,#0_9,#2_8,#0_8,#1_8,#4_7,#3_9,#4_6,#1_7,#0_7,#2_7,#0_6,#2_6,#1_6,#4_5,#3_8,#4_4,#1_5,#2_5,#0_5,#2_4,#1_4,#4_3,#3_7,#4_2,#1_3,#2_3,#0_4,#2_2,#1_2,#4_1,#3_6,#4_LiftOff,#1_1,#1_LiftOff,#2_1,
#0_3,#2_LiftOff,
#3_5,#3_4,#3_3,#3_2,#3_1,#3_LiftOff,
#0_2,#0_1,#0_LiftOff,
*/
可以看到,通过为id越大的线程设置越高的优先级,更潜性的改变任务的调度
3.除了以上两种方法,我们还可以通过设置让步Thread.yield()来告诉调度器让其他拥有相同优先级的线程先行。
共享受限资源
synchronized关键字
当多个任务同时访问同一个资源时,会发生不可预料的错误,我们看这个例子:
public class addThread implements Runnable{
private SynchronizeTest st;
public addThread(SynchronizeTest st){
this.st = st;
}
public void run() {
st.setsum();
System.out.println(st.getsum());
}
}
public class SynchronizeTest {
private int sum=0;
public int getsum() {
return sum;
}
public void setsum() {
sum = sum+1;
Thread.yield();
Thread.yield();
Thread.yield();
sum = sum+1;
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
SynchronizeTest st = new SynchronizeTest();
for(int i=0;i<5;i++){
exec.execute(new addThread(st));
}
exec.shutdown();
while(!exec.isTerminated()){}
System.out.println(st.getsum());
}
}/*
4
9
8
4
5
9*/
可以看到,结果是错误且不确定的,防止这种冲突的办法是加上锁防止多个线程同时操作一个对象,java中提供了synchronized关键字保护代码片段。
如果某个任务处于一个队标记为synchhronized方法的调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized方法的线程都会被堵塞。
如果对上述栗子中的getsum和setsum方法都加上synchronized关键字,则可以得到正常的结果。
lock对象
java中还提供了lock对象来实现锁,相比于synchronized关键字,该方式较为麻烦,以下是用lock对象实现setsum方法的锁:
private Lock lock = new ReentrantLock();
public void setsum() {
lock.lock();
try {
sum = sum+1;
Thread.yield();
Thread.yield();
Thread.yield();
Thread.yield();Thread.yield();
sum = sum+1;
} finally {
lock.unlock();
}
}
并且,使用lock对象能让我们更加细粒度的控制锁。
原子性与易变性
原子操作:不能内线程调度机制中端的操作
在java中除long和double的所有类型的简单操作都是原子操作,因为jvm对64位的读取和写入都是将其分为两个32位的操作来执行。
但如果使用volatile关键字,就能获得(简单的赋值与返回操作的)原子性。
并且,java中引入了诸如 AtomicInteger 、 AtomicLong 、AtomicReference 等特殊的原子类。
这些原子类能保证在机器级别上的原子性,比如,我们也可以用AtomicInteger来来代替int类型达到同步。
但是请注意:你的第一选择应该是synchronized关键字,其他方式都是有风险的。
临界区
有时候,为了提高效率,我们并不想对整个方法都加锁,synchronized关键字也同样支持对代码块加锁:
public void setsum() {
synchronized (this){
sum++;
Thread.yield();
Thread.yield();
Thread.yield();
sum++;
}
}
上面的代码等用于之前在方法前加synchronized关键字。但是请注意,当有一个对象有多个方法同步时,采用这种方式会导致不同线程对不同的方法之间也是互斥的,如果要实现临界资源访问独立,则需要创建多个类来施加多把锁:
private Object syncObject1 = new Object();
private Object syncObject1 = new Object();
public void f() {
synchronized (syncObject1){
···
}
}
public void g() {
synchronized (syncObject2){
···
}
}