【Java编程的逻辑】并发基础知识

线程的基本概念

线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈。
在Java中创建线程有两种方式:一种是继承Thread,另外一种是实现Runnable接口

public class HelloThread extends Thread{

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

}

public class HelloRunnable implements Runnable{

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

}

public class Test {

    public static void main(String[] args) {
        HelloThread helloThread = new HelloThread();
        helloThread.start();

        Thread helloRunnable = new Thread(new HelloRunnable());
        helloRunnable.start();
    }
}

通过继承Thread来实现线程虽然比较简单,但Java中只支持单继承,每个类最多只有一个父类。这时,可以通过实现java.lang.Runnable接口来实现线程。

线程的基本属性和方法

线程有一些基本属性和方法,包括id、name、优先级、状态、是否daemo线程、sleep方法、yield方法、join方法等

id 和 name

id是一个递增的整数,每创建一个线程就加一。
name的默认值是Thread-后跟一个编号。
name可以在Thread的构造方法中进行指定,也可以通过setName方法进行设置

// 获取当前线程name
Thread.currentThread().getName()
// 获取当前线程id
Thread.currentThread().getId()

优先级

线程有一个优先级的概念,在Java中,优先级从1到10,默认为5

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

这个优先级会被映射到操作系统中线程的优先级,不过因为操作系统各不相同,不一定都是10个优先级,Java中不同的优先级可能会被映射到操作系统中相同的优先级。

状态

Thread有一个方法用于获取线程的状态:

public State getState();

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

public enum State {
    // 没有调用start的线程
    NEW,
    // 调用start后线程在执行run方法且没有阻塞时。不过,RUNNABLE不代表CPU一定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片
    RUNNABLE,
    // 表示线程被阻塞了
    BLOCKED, WAITING, TIMED_WAITING,
    // 线程运行结束后
    TERMINATED;
}

daemon线程

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

最开始提到,启动线程会启动一条单独的执行流,整个程序只有在所有线程都结束的时候才退出,但daemon线程是例外,当整个程序中剩下的都是daemon线程的时候,程序就会退出。

daemon线程有什么用呢?它一般是其他线程的辅助线程,在它辅助的主线程退出的时候,它就没有存在的意义了。在我们运行一个即使最简单的程序,实际上Java也会创建多个线程,除了main线程,至少还有一个负责垃圾回收的线程,这个线程就是daemon线程,在main程序结束的时候,垃圾回收程序也会退出

sleep

public static native void slepp(long millis) throws InterruptedException;

调用该方法会让当前线程睡眠指定的时间,单位是毫秒。

睡觉期间,该线程会让出CPU,但睡眠的时间不一定的确切的给定毫秒数,会有一定偏差。
睡眠期间,线程可以被中断,如果被中断,sleep会抛出InterruptedException

yield

public static native void yield();

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

join

在最开始的使用示例中,可能HelloThread没有执行完,但是main线程执行完了,Thread有一个join方法,可以让调用join的线程等待该线程结束

public final void join() throws InterruptedException 

在等待线程结束的过程中,这个等待可能被中断,如果被中断,会抛出InterruptedException

join方法还有一个变体,可以限定等待的最长时间,单位为毫秒,如果为0,表示无限等待

public final synchronized void join(long millis) throws InterruptedException 

内存共享

每个线程表示一条单独的执行流,有自己的程序计数器、自己的栈。
但线程之间可以共享内存,它们可以访问和操作相同的对象

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>();
        ChildThread t1 = new ChildThread(list);
        ChildThread t2 = new ChildThread(list);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(shared);
        System.out.println(list);
    }
}
  1. 示例代码中有三条执行流,一条执行main方法,另外两条执行ChildThread的run方法
  2. 不同执行流可以访问和操作相同的变量
  3. 不同执行流可以执行相同的程序代码
  4. 当多条执行流执行相同的程序代码时,每条执行流都有单独的栈,方法中的参数和局部变量都有自己的一份

竞态条件

所谓竞态条件(race condition)是指,当多个线程访问和操作同一个对象时,最终执行结果与执行时序有关

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

}

期望结果是1000万,但实际执行会发现每次输出的结果都不一样,一般都不会是1000万。
因为counter++这个操作不是原子操作, 它分为三个步骤
1. 取counter的当前值
2. 在当前值基础上加1
3. 将新值重新赋值给counter

两个线程可能同时执行第一步,取到了相同的counter值,比如都取到了100,第一个线程执行完之后counter变为101,而第二个线程执行完后还是101。

解决办法:
1. 使用synchronized关键字
2. 使用显示锁
3. 使用原子变量

内存可见性

多个线程可以共享访问和操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上就能看到,甚至永远都看不到

public class VisibilityDemo {

    private static boolean shutdown = false;
    static class HelloThread extends Thread {
        @Override
        public void run() {
            while(!shutdown) {
                // do something
            }
            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");
    }
}

期望的结果是两个线程都退出,但实际执行时,很可能会发现HelloThread永远都不会退出。这就是内存可见性问题
在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也可能先写到缓存中,稍后才会同步更新到内存中。所以,一个线程对内存的修改,另一个线程看不到,一个修改没有及时同步到内存,二是另一个线程根本就没有从内存读。

解决办法:
1. 使用volatile关键字
2. 使用synchronized关键字或显示锁同步

猜你喜欢

转载自blog.csdn.net/u013435893/article/details/80018690