java多线程开发基础

来源:互联网 发布:初学者编程 编辑:程序博客网 时间:2024/05/22 15:16

多线程的实现方式

在目前的jdk版本中,多线程的实现方式有以下三种

1.继承Thread

这种方式代码很简单,我们只需要自定义一个线程类,让它继承Thread,并复写它的run方法即可:

public class MyThread extends Thread{    @Override    public void run() {        System.out.println("支线程开启。。。。。。");    }}

测试类如下:

public class ThreadTest {    public static void main(String[] args) throws InterruptedException {        System.out.println("主线程执行。。。。。");        MyThread myThread = new MyThread();        myThread.start();    }}

多线程的执行一般都是异步的,也就是说,当支线程开启之后,主线程是不会等待支线程执行的,而是继续执行下面的代码,当主线程执行完成之后,如果支线程还没有结束,那么主线程会等待支线程执行完成之后,再结束程序。

2.实现runnable接口

其实这种方式实现多线程算是第一种方式的另外一种形式,我们可以查看Thread类,发现这个类也是实现了Runnable这个接口的,并且重写了run方法,我们可以看看Thread是怎么重写的:

 @Override    public void run() {        if (target != null) {            target.run();        }    }

所以我们在调用Thread的run方法的时候,其实调用的是target的run方法,我们可以发现target就是一个Runnable接口的实现类,所以我们只需要实现Runnable接口,并且实现其run方法,并将这个实现类当成一个属性赋值给Thread即可:

public class MyRunnable implements Runnable{    @Override    public void run() {        System.out.println("支线程执行。。。。。");    }}

开启线程方法如下:

public class ThreadTest {    public static void main(String[] args) throws InterruptedException {        MyRunnable myRunnable = new MyRunnable();        Thread thread = new Thread(myRunnable);        thread.start();    }}

其实这两种方法的实现原理是一样的,只不过一个是通过继承的方式,一个是通过实现接口的方式。

3.实现callable接口

在jdk1.5之后,推出了另外一种实现多线程的方式,那就是callable接口,这个接口算是Runnable接口的一种扩展吧,它能接收支线程返回数据,并且是可以抛出异常的。具体实现代码如下:

public class MyCallable implements Callable<String>{    @Override    public String call() throws Exception {        System.out.println("支线程执行。。。。。");        Thread.sleep(1000);        return "ok";    }}

测试类代码:

public class ThreadTest {    public static void main(String[] args) throws InterruptedException, ExecutionException {        MyCallable mCallable = new MyCallable();        FutureTask<String> future = new FutureTask<String>(mCallable);        new Thread(future).start();        System.out.println(future.get());        System.out.println("主线程执行结束。。。。。");    }}

当我们执行future的get方法的时候,主线程会阻塞等待支线程执行完成,FutureTask这个类其实就是Runnable和Future接口的实现。

线程的状态

以上的部分只是多线程的简单实现方式,下面来说说线程的几种状态,如下图所示:
这里写图片描述

看图可知,线程主要有以下几种状态:
1. 新建状态:即刚刚创建的线程实例。
2. 就绪状态:当创建的线程实例调用其start()方法的时候,这个线程就处于就绪状态,随时可以被调度。
3. 运行状态:当就绪线程获取到CPU资源的时候,线程中的方法被执行。
4. 冻结状态:运行中的线程中调用了sleep或者wait等方法,就会进入此状态,处于这种状态下的线程是不会获取到CPU的调度的。
5. 死亡状态:当线程运行结束或者在运行过程中发生异常的时候,线程就会消亡。

线程各个状态之间的转换可参考上图。

线程对象中常用的方法

interrupted:判断当前这个线程是否发生中断。

interrupt:当线程处于等待状态(sleep,wait,join)的时候,可以通过这个方法强行将该线程唤醒,但是会抛出:java.lang.InterruptedException,其原理就是将该线程的一个状态置为true,也就是上面的interrupted判断的那个状态。正常运行的代码是不会检测这个状态的,但是处于等待状态的会时刻检测,所以正常运行的代码不会抛出异常。

public class MyCallable implements Callable<String>{    @Override    public String call() throws Exception {        while (!Thread.interrupted()) {            System.out.println("支线程运行中。。。。。");        }        return "over";    }}

在主线程中我们可以通过interrupt方法控制该支线程的运行。

join:当我们的主线程开启支线程的,我们不能保证当支线程已开启就调用支线程,这个取决于CPU的调度,但是我们可以在启用支线程的start方法之后,调用其join方法,保证在支线程运行完成之后在运行主线程,这个有点类似于单线程了。

yield:将当前线程的cpu使用权转让出去。

多线程的内存模型

任何形式的开发都离不开对数据的操作,多线程也是如此。每个线程都有自己独立的空间,也就是线程栈。当线程操作数据的时候,会在主内存中读取数据进行操作,大致流程如下图:
这里写图片描述

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一个线程独占的状态
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传送到线程中的工作内存,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存中的变量
  • store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便随后的write操作
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值写入主内存的变量中

Java内存模型还规定了执行上述8种基本操作时必须满足如下规则

  1. 不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
  2. 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  3. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
  4. 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  5. 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
  6. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  7. 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
  8. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。

由上图可知,每个线程都有自己的工作内存,当我们需要对数据进行操作的时候,从主内存中将数据加载进来,并进行操作,操作完成之后再写出到主内存,但是,由于工作空间具有缓存的作用,所以当第二次操作该数据的时候,会直接使用缓存中的数据,这样如果在此之前,主内存中的该数据被修改了,就和缓存中的不一样了,这样就出现了线程安全问题。

volatile关键字的作用

保证了新值能立即存储到主内存,每次使用前立即从主内存中刷新。
禁止指令重排序优化。
注:volatile关键字不能保证在多线程环境下对共享数据的操作的正确性。可以使用在自己状态改变之后需要立即通知所有线程的情况下。
当然,使用volatile也不能保证线程的安全,这个关键字只能保证可见性,但是不能保证原子性。看下面这段代码:

public class MyCallable implements Callable<Integer>{    public static volatile int a = 0;    @Override    public Integer call() throws Exception {        a++;        return a;    }}

测试类:

public class ThreadTest {    public static void main(String[] args) throws InterruptedException, ExecutionException {        for (int i = 0; i < 1000; i++) {            MyCallable mCallable = new MyCallable();            FutureTask<Integer> future = new FutureTask<Integer>(mCallable);            Thread thread = new Thread(future);            thread.start();        }        System.out.println(MyCallable.a);    }}

我们会发现,运行的结果不是1000。说明存在线程安全问题。那么问题出在什么地方呢?虽然对a进行操作的时候都会去主内存中获取最新的数据,但是这其中要进行两步,read和load,这两条指令之间是可以发生其他事情的,比如,另一个线程将最新的数据写入了主内存。
所以,如果在线程安全的时候,就不要使用这个关键字了。因为它会大大的降低程序的运行效率。

原创粉丝点击