Java进阶(二)并发(上篇)

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){
            ···
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值