Loading

并发基础知识之线程的基本概念

1.创建线程

线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈。

创建线程有两种方式:一种是继承Thread,另外一种是实现Runnable接口。


(1)继承Thread

Java中java.lang.Thread这个类表示线程,一个类可以继承Thread并重写run方法来实现一个线程。

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello");
    }
}

线程需要启动的话,得执行start方法。

public static void main(String[] args) {
    Thread thread = new MyThread();
    thread.start();
}

如果不执行start方法,那么run方法就只是一个普通的方法。

每个线程都有一个id和name,要获取它们,可以使用getId()和getName()方法,要获取当前执行的线程,可以使用currentThread()方法。

修改上面的run方法

@Override
public void run() {
    System.out.println("thread name:" + Thread.currentThread().getName());
    System.out.println("hello");
}

如果在main方法中通过start方法启动线程,则输出

thread name:Thread-0
hello

如果直接调用run方法,则输出

thread name:main
hello

调用start方法后,就有两条执行流,一条执行run方法,一条执行main方法,两条执行流并发执行,操作系统负责调度。


(2)实现Runnable接口

通过继承Thread来实现线程比较简单,但java只支持单继承,这时就可以通过实现Runnable接口来实现线程,Runnable接口只有一个run方法。

public class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("hello");
    }
}

启动线程同样要使用start方法,不过创建一个线程的方式有所不同

public static void main(String[] args) {
    Thread thread = new Thread(new MyThread());
    thread.start();
}

2.线程的基本属性和方法

(1)id和name

前面我们已经说过了,每个线程都有一个id和name,id是一个递增的整数,每创建一个线程就加1.name的默认值是Thread-后面跟一个编号,name可以在Thread的构造方法中指定。也可以通过setName方法进行设置,给Thread设置一个友好的名字,可以方便测试。

(2)优先级

线程有一个优先级的概念,优先级从1到10,默认是5,可以通过setPriority方法设置。不同的操作系统略有不同。

public final void setPriority(int newPriority)
public final int getPriority()

(3)状态

线程有一个状态的概念,Thread有一个getState方法可以获取线程的状态。

public State getState()

返回值为Thread.State,它是一个枚举类型,有如下值

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

NEW:没有调用的线程状态。
RUNNABLE:调用start后线程在执行run方法且没有阻塞。
TERMINATED:线程运行结束后状态为TERMINATED。
BLOCKED,WAITING,TIMED_WAITING:都表示线程被阻塞了,在等等一些条件。

Thread还有一个方法,表示线程是否还活着

public final native boolean isAlive()

线程被启动后,run方法运行结束前,此方法返回值都是true。

(4)是否是daemon线程

Thread有一个是否daemon线程的属性,相关方法:

public final void setDaemon(boolean on)
public final boolean isDaemon()

前面我们说了,启动线程会启动一条单独的执行流,整个程序只有在所有线程都执行完毕后才会退出,但daemon线程除外,当整个程序剩下的都是daemon线程时,程序就会退出。

daemon线程其实就是后台进程,比如垃圾收集器。作用就是辅助其他的线程,当其他的线程执行完毕后,它们也就没有存在的必要了。

(5)sleep方法

调用该方法可以使当前线程睡眠,但不释放对象锁。(wait方法会释放对象锁)

public static void **sleep**(long millis) throws InterruptedException

(6)yield方法

Thread还有一个让出CPU的方法

public static native void yield()

调用该方法,是告诉系统的调度器:我现在不着急使用CPU,你可以让其他线程先运行。

(7)join方法

调用该方法,可以让其他线程等待调用这个方法的线程结束。

public final void join() throws InterruptedException

比如前面的main线程和thread线程,如果希望main线程在thread线程结束后再退出,则加上

thread.join()

3.共享内存及可能存在的问题

(1)共享内存

public class ShareMemoryDemo {
    private static int shared = 0;
    private static void incrShared() {
        shared++;
    }

    static class ChildThread extends Thread {
        List<String> list;
        public ChildThread(List<String> list) {
            this.list = list;
        }

        @Override
        public void run() {
            incrShared();
            list.add(Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        Thread t1 = new ChildThread(list);
        Thread t2 = new ChildThread(list);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(shared);
        System.out.println(list);
    } 
}

大部分情况下,会输出如下

2
[Thread-0, Thread-1]

通过这个例子,说明执行流、内存和代码的关系如下:

  • 该代码中有三条执行流,一条执行main方法,另外两条执行ChildThread 的run方法。
  • 不同执行流可以访问和操作相同的变量,如本例中的shared和list变量。
  • 不同的执行流可以执行相同的代码。
  • 当多条执行流执行相同的d代码时,每条执行流都有单独的栈,方法中的参数和局部变量都有自己的一份。

(2)竞态条件

竞态条件是指当多个线程访问和操作同一变量的时候,执行的结果和执行次序有关,结果可能正确也可能不正确。

public class CounterThread extends Thread {
    private static int counter = 0;
    @Override
    public void run() {
        for(int i = 0; i < 1000; i++) {
            counter++;
        } 
    }

    public static void main(String[] args) throws InterruptedException {
        int num = 1000;
        Thread[] threads = new Thread[num];
        for(int i = 0; i < num; i++) {
            threads[i] = new CounterThread();
            threads[i].start();
        }
        for(int i = 0; i < num; i++) {
            threads[i].join();
        }
        System.out.println(counter);
    }
}

期望的结果是1000*1000,但运行输出一般不是这个数。

因为counter++不是一个原子操作,主要分成以下三步

  1. 取得counter的值
  2. counter的值加1
  3. 将新值重新赋给counter

这样的情况下,如果我们两个线程同时取得了counter的值,比如100,那么第一个线程执行完后counter的值为101,第二个线程执行完后,counter的值还是101,这肯定会与我们的期望值不一样。

解决这个问题的方法有:

  • 使用synchronized关键字
  • 使用显式锁
  • 使用原子变量

这个在后面的文章中我会一一讲。

(3)内存可见性

所谓内存可见性,就是一个线程对变量的修改,其他的线程能够马上看到。因为多个线程可以共享和操作相同的变量,所以如果一个线程对变量的修改,其他的线程不能马上看到,甚至永远也看不到,这就会出错了。

public class VisibilityDemo {
    private static boolean shutdown = false;
    static class HelloThread extends Thread {
        @Override
        public void run() {
            while(!shutdown) {
                System.out.println("1");
            }
            System.out.println("exit hello");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new HelloThread().start();
        Thread.sleep(1000);
        shutdown = true;
        System.out.println("exit main");
    }
}

上面的程序中,一开始shutdown变量的值为false,这样启动线程后就会一直打印1,在main方法中,启动线程后,我们休息一下,然后把shutdown的值设为true,期望应是停止打印1,但结果可能是一直循环下去。

这就是内存可见性问题,在计算机系统中,除了内存,数据还会被缓存到cup的寄存器以及各级缓存中,当访问一个变量的时候,可能是直接从寄存器或缓存中读取,当修改变量的时候,也可能是先写到缓存中,之后才会同步更新到内存中,这是延迟写。在单线程中,这一般没问题,但在多线程中,就可能出现很严重的问题,一个线程对另一个变量的修改,另一个线程看不懂,一是没有及时同步到内存,二是另一个线程根本没从内存中读。

解决这个问题,有以下方法:

  • 使用volatile关键字
  • 使用synchronized关键字或显式锁同步。

4.线程的优缺点

优点:

  • 能充分利用多cpu的能力。
  • 充分利用硬件资源,cpu和硬盘。
  • 简化模型及IO处理

缺点:

线程的调度和切换都需要花费时间,当有大量可运行线程的时候,操作系统会忙于调度,为一个线程分配一段时间,执行完后,再让另一个线程执行,一个线程被切换出去,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前cpu寄存器的值,程序计数器的值等,当一个线程被切换回来后,操作系统又要恢复它的上下文,整个过程就是上下文切换。这个切换不仅耗时,而且使cpu中的好多缓存失效。


往期文章

并发基础知识之synchronized关键字
并发基础知识之线程间的协作

posted @ 2018-05-17 13:28  CodeTiger  阅读(31)  评论(0编辑  收藏  举报