美文网首页Java面试
多线程篇一(Java多线程基础)

多线程篇一(Java多线程基础)

作者: Albert_Yu | 来源:发表于2018-06-20 17:54 被阅读74次

    前言
    该篇文章主要是书写一些多线程相关的基础知识。

    正文
    一、进程与线程
    进程:直白的讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启的一个进程。
    线程:线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。
    对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。
    进程间是独立的,这表现在内存空间,上下文环境;线程运行在进程空间内。一般来讲(不使用特殊技术)进程是无法突破进程边界存取其它进程内的存储空间;而线程由于处于进程空间内,所以同一进程所产生的线程共享同一内存空间。

    二、线程的优劣

    1、线程的优势
    1)、充分发挥多处理器的强大能力(提高处理器资源的利用率来提升系统吞吐率)
    如果在一个程序中只有一个线程,那么同时最多只能在一个处理器上运行,在双处理器系统上,单线程程序只能使用一半的CPU资源,而在拥有100个处理器的系统上,将有99%的资源无法使用。
    使用多线程还有助于在单处理器系统上获得更高的吞吐率。如一个线程在等待I/O操作完成时,另一个线程可以继续运行,使程序在I/O阻塞期间继续运行。
    2)、建模的简单性
    举个例子:如果在程序中只包含一种类型的任务,那么比包含多种不同类型任务的程序要更易于编写,错误更少,也更容易测试。通过使用线程可以把复杂并且异步的工作流进一步分解为一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置进行交互。
    3)、异步事件的简化处理
    4)、响应更灵敏的用户界面
    2、线程的风险
    1)、安全性问题
    在多线程的环境下,由于同一进程下线程共享同一内存空间,当多个线程同时对共享数据进行操作的时候很可能会出现非预期的情况,如下代码片段:

        // 统计accessCount方法被访问的次数
        private long count;
        // 并发的线程数
        private static final int THREAD_COUNT = 10;
    
        private CountDownLatch latch = new CountDownLatch(1);
    
        public void accessCount() throws InterruptedException {
            // 做一些业务逻辑处理
            System.out.println("线程"+Thread.currentThread().getName()+"获取到的count="+ ++count);
        }
    
        private static class AccessCountThread extends Thread {
            private UnSafeCountingFactory factory;
    
            private AccessCountThread(UnSafeCountingFactory factory) {
                this.factory = factory;
            }
    
            @Override
            public void run() {
                try {
                    factory.latch.await();
                    factory.accessCount();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        private static class AccessCountThreadTest{
            public static void main(String[] args) throws InterruptedException {
                UnSafeCountingFactory factory = new UnSafeCountingFactory();
                for (int i = 0; i < THREAD_COUNT; i++) {
                    Thread thread = new AccessCountThread(factory);
                    thread.start();
                }
                Thread.sleep(1000);
                factory.latch.countDown();
            }
        }
    

    程序的执行结果

    线程Thread-1获取到的count=3
    线程Thread-8获取到的count=6
    线程Thread-0获取到的count=4
    线程Thread-2获取到的count=5
    线程Thread-4获取到的count=5
    线程Thread-6获取到的count=2
    线程Thread-7获取到的count=7
    线程Thread-3获取到的count=8
    线程Thread-9获取到的count=10
    线程Thread-5获取到的count=9
    

    分析出现两个5的一种原因:++count实际上包含了3个独立的操作:读取count、将count+1、并将结果写入count。在这3个非原子操作的步骤中,可能有两个线程同时执行读操作,从而它们得到相同的值,并都将这个值加1。


    Image 1.png

    2)、活跃性问题
    在多线程环境下可能会发生死锁等问题
    3)、性能问题
    在多线程程序中,当线程调度器挂起活跃线程并转而运行另一个线程时,就会频繁出现上下文切换操作,这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不是线程运行上。
    三、多线程常用基础知识
    1、创建线程
    java中创建线程有如下几种方式:
    (1)、自定义类继承Thread类,重写Thread类中的run方法

    public class HowStartThread {
    
        private static class TestThread extends Thread {
            @Override
            public void run() {
                System.out.println("TestThread is running");
            }
        }
    
        public static void main(String[] args) {
            Thread t1 = new TestThread();
            t1.start();
        }
    }
    

    (2)、自定义类实现Runnable接口,实现Runnable接口中的run方法

    public class HowStartThread {
    
        private static class TestRunnable implements Runnable {
    
            @Override
            public void run() {
                System.out.println("TestRunnable is running");
            }
        }
    
        public static void main(String[] args) {
            Thread t2 = new Thread(new TestRunnable());
            t2.start();
        }
    }
    

    (3)、结合JDK线程池技术(ThreadPoolExecutor或ExecutorService)、Callable、Future实现有返回值的线程

    public class HowStartThread {
    
        private static class TestCallable implements Callable<Integer>{
            @Override
            public Integer call() {
                System.out.println("TestCallable is running");
                return 1;
            }
        }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            // 创建一个使用从无界队列运行的单个工作线程的执行程序。
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            // 提交一个Callable任务
            Future<Integer> testCallable = executorService.submit(new TestCallable());
            // get方法会阻塞TestCallable线程执行完成
            Integer integer = testCallable.get();
            System.out.println(integer);
            // 关闭线程池
            executorService.shutdown();
        }
    }
    

    2、线程完成
    (1)、run方法执行完成
    (2)、抛出一个未处理的异常,导致线程提前结束
    3、线程的状态
    java中在Thread类中的枚举类State中定义了如下几种线程的状态
    (1)、新创建NEW:线程被创建,但是还没有调用start方法
    (2)、可运行RUNNABLE:可运行的线程状态在Java虚拟机中执行,但它可能执行等待操作系统的其他资源如处理器。
    (3)、阻塞BLOCKED:线程被阻塞于锁,等待监视器锁:同步代码块,I/O操作,lock等。
    (4)、等待WAITING:由于调用以下方法之一,线程处于等待状态:Object#wait(),join()
    (5)、计时等待TIMED_WAITING:线程处于定时等待状态,因为它调用了下列方法之一,并指定了正等待时间:Thread.sleep,Object#wait(long),join(long),LockSupport#parkNanos,LockSupport#parkUntil
    (6)、被终止TERMINATED:线程执行完成

    下面举个例子来通过idea来查看线程的状态

    /**
     * @Author YUBIN
     * @create 2018-01-07
     */
    public class SleepUtils {
        public static final void second(long seconds) {
            try {
                TimeUnit.SECONDS.sleep(seconds);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    /**
     * 线程状态的演示
     *
     * @Author YUBIN
     * @create 2018-01-07
     */
    public class ThreadState {
        private static Lock lock = new ReentrantLock();
    
        public static void main(String[] args) {
            new Thread(new SleepAlways(), "SleepAlwaysThread").start();
            new Thread(new Waiting(),"WaitingThread").start();
            // 使用两个Blocked线程,一个获取锁成功,另外一个被阻塞
            new Thread(new Blocked(),"BlockedThread-1").start();
            new Thread(new Blocked(),"BlockedThread-2").start();
            new Thread(new Sync(),"SyncThread-1").start();
            new Thread(new Sync(),"SyncThread-2").start();
            new Thread().setPriority(5);
        }
    
        /**
         * 该线程不断的进行睡眠
         */
        private static class SleepAlways implements Runnable {
            @Override
            public void run() {
                while (true) {
                    SleepUtils.second(10000);
                }
            }
        }
    
        /**
         * 该线程在Waiting.class 实例上等待
         */
        private static class Waiting implements Runnable {
            @Override
            public void run() {
                while (true) {
                    synchronized (Waiting.class) {
                        try {
                            Waiting.class.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    
        /**
         * 该线程在Blocked.class加锁后,不会释放锁
         */
        private static class Blocked implements Runnable {
            @Override
            public void run() {
                synchronized (Blocked.class) {
                    while (true) {
                        SleepUtils.second(1000);
                    }
                }
            }
        }
    
        /**
         * 该线程获得锁喉又释放锁
         */
        private static class Sync implements Runnable {
            @Override
            public void run() {
                lock.lock();
                try {
                    SleepUtils.second(3000);
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    

    启动main方法,点击控制台处的小相机按钮可查看线程的状态

    线程状态图.png

    4、线程的常用方法
    (1)、start方法和run方法
    start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
    run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
    (2)、sleep方法
    sleep方法的作用是在指定的毫秒数内让当前正在执行的线程休眠。
    在Thread类中有两个重载的sleep方法

    sleep(long millis)     //参数为毫秒
    sleep(long millis,int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒
    

    sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
    但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
    (3)、wait、notify和notifyAll
    这3个方法在调用以前,当前线程必须持有锁。调用了wait()、notify()、notifyAll()会释放锁
    案例:结合wait()和notifyAll()自定义一个有界队列

    /**
     * 自定义有界队列
     *
     * @Author YUBIN
     * @create 2018-01-10
     */
    public class BlockQueueWN<T> {
    
        private List blockQueue = new LinkedList<>();
    
        private final int limit;
    
        public BlockQueueWN(int limit) {
            this.limit = limit;
        }
    
        // 入队 对象锁
        public synchronized void enqueue(T t) throws InterruptedException {
            while (blockQueue.size() == limit) {
                // 当队列满的时候等待
                wait();
            }
            // 将数据入列之前如果队列为空,那么出队的队列可能正在等待,需要执行唤醒操作
            if (blockQueue.size() == 0) {
                notifyAll();
            }
            blockQueue.add(t);
        }
    
        // 出队 对象锁
        public synchronized T dequeue() throws InterruptedException {
            while (blockQueue.size() == 0) {
                // 当队列为空的时候等待
                wait();
            }
            // 将数据出列之前如果队列是满的,那么入队的队列可能正在等待,需要执行唤醒操作
            if (blockQueue.size() == limit) {
                notifyAll();
            }
            T t = (T)blockQueue.remove(0);
            return t;
        }
    }
    

    测试类

    public class BqTest {
        public static void main(String[] args) {
            BlockQueueWN bq = new BlockQueueWN(10);
            Thread threadA = new ThreadPush(bq);
            threadA.setName("Push");
            Thread threadB = new ThreadPop(bq);
            threadB.setName("Pop");
            threadB.start();
            threadA.start();
        }
    
        /**
         * 往队列中插入元素
         */
        private static class ThreadPush extends Thread {
            private BlockQueueWN<Integer> blockQueue;
    
            private ThreadPush(BlockQueueWN<Integer> blockQueue) {
                this.blockQueue = blockQueue;
            }
    
            @Override
            public void run() {
                int i = 20;
                while (i > 0) {
                    try {
                        System.out.println("i=" + i + " will push");
                        blockQueue.enqueue(i--);
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        private static class ThreadPop extends Thread {
            private BlockQueueWN<Integer> blockQueue;
    
            private ThreadPop(BlockQueueWN<Integer> blockQueue) {
                this.blockQueue = blockQueue;
            }
    
            @Override
            public void run() {
                while(true){
                    try {
                        System.out.println(Thread.currentThread().getName() + " will pop.....");
                        Integer i = blockQueue.dequeue();
                        System.out.println(" i=" + i.intValue() + " alread pop");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    (4)、yield()方法
    当前线程出让CPU使用权,当前线程变成Runnable状态,下一刻仍然可能被CPU选中,它与sleep一样不会释放锁。(个人感觉这个方法有点鸡肋,如有合适的应用场景请留言)
    (5)、join()、join(long millis)方法
    等待这个线程死亡,或者等待这个线程死亡最多 等待millis毫秒。
    在很多情况下,主线程创建并启动了线程,如果子线程中要进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再进行某些操作,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁。
    注意:join()不会释放锁
    代码演示:

    /**
     * join方法演示
     *
     * @Author YUBIN
     * @create 2018-06-20
     */
    public class JoinTest {
        private static class JoinThread extends Thread {
    
            private boolean flag;
    
            private JoinThread(boolean flag) {
                this.flag = flag;
            }
    
            @Override
            public void run() {
                if (flag) {
                    // 执行数据库插入操作
    
                    System.out.println(getName() + "数据库插入操作完成");
                } else {
                    // 执行缓存操作
                    System.out.println(getName() + "缓存操作执行完成");
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread threadA = new JoinThread(true);
            Thread threadB = new JoinThread(false);
            threadA.setName("ThreadA");
            threadB.setName("ThreadB");
            threadA.start();
            threadB.start();
            // 主线程上的一些自己的操作
            System.out.println("主线程自己的业务逻辑执行完成");
            // 等待线程A消亡
            threadA.join();
            // 等待线程B消亡
            threadB.join();
            System.out.println("主线程结合线程A和线程B处理一些业务");
        }
    }
    

    5、线程优先级
    Thread类中的成员变量priority控制优先级,范围1-10之间,缺省为5.可以通过setPriority()设置线程的优先级,数字越大优先级越高。

        /**
         * The minimum priority that a thread can have.
         */
        public final static int MIN_PRIORITY = 1;
    
       /**
         * The default priority that is assigned to a thread.
         */
        public final static int NORM_PRIORITY = 5;
    
        /**
         * The maximum priority that a thread can have.
         */
        public final static int MAX_PRIORITY = 10;
    

    对于线程优先级,我们需要注意:
    Thread.setPriority()可能根本不做任何事情,这跟你的操作系统和虚拟机版本有关
    线程优先级对于不同的线程调度器可能有不同的含义,可能并不是你直观的推测。特别地,优先级并不一定是指CPU的分享。在UNIX系统,优先级或多或少可以认为是CPU的分配,但Windows不是这样
    线程的优先级通常是全局的和局部的优先级设定的组合。Java的setPriority()方法只应用于局部的优先级。换句话说,你不能在整个可能的范围 内设定优先级。(这通常是一种保护的方式,你大概不希望鼠标指针的线程或者处理音频数据的线程被其它随机的用户线程所抢占)
    不同的系统有不同的线程优先级的取值范围,但是Java定义了10个级别(1-10)。这样就有可能出现几个线程在一个操作系统里有不同的优先级,在另外一个操作系统里却有相同的优先级(并因此可能有意想不到的行为)
    操作系统可能(并通常这么做)根据线程的优先级给线程添加一些专有的行为(例如”only give a quantum boost if the priority is below X“)。这里再重复一次,优先级的定义有部分在不同系统间有差别。
    大多数操作系统的线程调度器实际上执行的是在战略的角度上对线程的优先级做临时操作(例如当一个线程接收到它所等待的一个事件或者I/O),通常操作系统知道最多,试图手工控制优先级可能只会干扰这个系统。
    你的应用程序通常不知道有哪些其它进程运行的线程,所以对于整个系统来说,变更一个线程的优先级所带来的影响是难于预测的。例如你可能发现,你有一个预期 为偶尔在后台运行的低优先级的线程几乎没有运行,原因是一个病毒监控程序在一个稍微高一点的优先级(但仍然低于普通的优先级)上运行,并且无法预计你程序 的性能,它会根据你的客户使用的防病毒程序不同而不同。
    注意:不要假定高优先级的线程一定先于低优先级的线程执行,不要有逻辑依赖于线程优先级,否则可能产生意外结果。
    6、守护线程Daemon
    守护线程(如GC线程),程序里没有非守护线程时,java程序就会推出。一般用不上,也不建议我们平时开发的时候使用,因为try/finally中的代码不一定会执行,如下代码演示,如果将t1.setDaemon(true)代码放开,则线程中的打印语句则不会执行。

    /**
     * 演示守护线程
     * @Author YUBIN
     * @create 2018-01-07
     */
    public class DaemonThread {
        private static class TestRunnable implements Runnable {
    
            @Override
            public void run() {
                try {
                    SleepUtils.second(2);
                }finally {
                    System.out.println("TestRunnable is Daemon");
                }
            }
        }
    
        public static void main(String[] args) {
            Thread t1 = new Thread(new TestRunnable());
            //t1.setDaemon(true);
            t1.start();
        }
    }
    

    7、线程的中断interrupt
    (1)、不安全的取消
    单独使用一个取消标识(如果当前线程处于阻塞状态是无法判断标识位的)
    stop()、suspend()、resume()是过期的api,很大的副作用,容易导致死锁或者数据不一致的问题
    (2)、安全的终止线程
    使用线程的中断:
    interrupt():中断线程,本质将线程的中断标志位设为true,其它线程向需要中断的线程打个招呼,是否真正进行中断由线程自己决定
    isInterruped():线程检查自己的中断标志位,如果未中断返回false
    interrupted():这是一个静态的方法,判断线程是否中断。

    演示安全的终止线程

    /**
     * 安全的中断线程
     *
     * @Author YUBIN
     * @create 2018-01-07
     */
    public class SafeInterrupt {
    
        private static Object o = new Object(); // 锁对象
    
        private static class TestThread extends Thread {
    
            private volatile boolean on = true;
    
            private Socket socket;
    
            private long i = 0;
    
            @Override
            public void run() {
                while (on && !Thread.currentThread().isInterrupted()) {
                    System.out.println(i++);
                    try {
                        synchronized (o) {
                            o.wait();
                        }
                    } catch (InterruptedException e) {
                        // 当wait方法检测到当前线程被中断了,则抛出中断异常,同时将标志位设为false
                        System.out.println("当前执行线程的中断标志位:" + Thread.currentThread().getId() + "," + Thread.currentThread().isInterrupted());
                        // 重新设置一下中断标志位
                        interrupt();
                        System.out.println("被中断的线程_"+getId()+":"+isInterrupted());
                    }
                }
                System.out.println("TestRunnable is stop!" + i);
            }
    
            public void cancle() {
                System.out.println("Ready stop thread......");
                on = false;
                interrupt(); // 注意此处不能使用Thread.currentThread().interrupt()  这个方法是main线程调用的
            }
        }
    
        public static void main(String[] args) {
            TestThread t1 = new TestThread();
            t1.start();
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t1.cancle();
        }
    }
    

    运行结果

    0
    Ready stop thread......
    当前执行线程的中断标志位:11,false
    被中断的线程_11:true
    TestRunnable is stop!
    1
    

    当在main方法中调用线程的cancle()方法是,将标识位设为false同时将中断标识位设为true;如果在while判断中没有中断标识位的判断,该线程是无法被终止的。
    wait()方法在阻塞的过程中会循环的判断当前线程的中断标识位是否为false,未过不为false的时候会抛出中断异常,同时会将中断标识位设为false,因此在上述的代码的catch语句中需要将线程重新中断下。

    总结


    多线程.png

    相关文章

      网友评论

      本文标题:多线程篇一(Java多线程基础)

      本文链接:https://www.haomeiwen.com/subject/aoqtyftx.html