1 多线程的优缺点

之前写的都乱糟糟的,现在也需要重新记忆一遍。所以重新整理一下JUC包。

多线程及其优缺点

什么是线程

是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。(wiki百科)

创建线程的三种方式

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        //1、继承Thread方式
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                System.out.println("thread1 start");
            }
        };
        thread1.start();

        //2、实现Runnable接口
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread2 start");
            }
        });
        thread2.start();

        //3、实现Callable接口
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future future = executorService.submit(new Callable() {
            @Override
            public String call() throws Exception {
                return "future start";
            }
        });
        try {
            String result = future.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

在jdk8之后用lambda表达式转换一下

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        //1、继承Thread方式
        Thread thread1 = new Thread(() -> System.out.println("thread1 start"));
        thread1.start();

        //2、实现Runnable接口
        Thread thread2 = new Thread(() -> System.out.println("thread2 start"));
        thread2.start();

        //3、实现Callable接口
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future future = executorService.submit(() -> "future start");
        try {
            String result = future.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

简化了一点,但是更多是有点懵,lambda为什么会简化方法,->是怎么找到对应的方法,下次在研究。

为什么要用多线程

早期的CPU是单核的,为了提升计算能力,将多个计算单元整合到一起。形成了多核CPU。多线程就是为了将多核CPU发挥到极致,一边提高性能

多线程缺点呢

上面说了多线程的有点是:为了提高计算性能。那么一定会提高?
答案是不一定的。有时候多线程不一定比单线程计算快。引入《java并发编程的艺术》上第一个例子

public class ConcurrencyTest {

    /** 执行次数 */
    private static final long count = 10000l;

    public static void main(String[] args) throws InterruptedException {
        //并发计算
        concurrency();
        //单线程计算
        serial();
    }

    private static void concurrency() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (long i = 0; i < count; i++) {
                    a += 5;
                }
                System.out.println(a);
            }
        });
        thread.start();
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        thread.join();
        long time = System.currentTimeMillis() - start;
        System.out.println("concurrency :" + time + "ms,b=" + b);
    }

    private static void serial() {
        long start = System.currentTimeMillis();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a += 5;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
    }

}

结果为

50000
concurrency :22ms,b=-10000
serial:0ms,b=-10000,a=50000

而且多线程会带来额外的开销

  • 上下文切换
  • 线程安全

上下文切换

时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。
减少上下文切换可以采用无锁并发编程,CAS算法,使用最少的线程和使用协程。

  • 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
  • CAS算法,利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换
  • 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

线程安全的问题

多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况
同样引入《java并发编程的艺术》的一个例子

public class DeadLockDemo {
    private static String resource_a = "A";
    private static String resource_b = "B";

    public static void main(String[] args) {
        deadLock();
    }

    public static void deadLock() {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resource_a) {
                    System.out.println("get resource a");
                    try {
                        Thread.sleep(3000);
                        synchronized (resource_b) {
                            System.out.println("get resource b");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resource_b) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("get resource b");
                    synchronized (resource_a) {
                        System.out.println("get resource a");
                    }
                }
            }
        });
        threadA.start();
        threadB.start();

    }
}

然后通过jps查看,找个这个类的id
然后通过jstack id来查看

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x0000000016074808 (object 0x00000000e0b89280, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x0000000016075ca8 (object 0x00000000e0b892b0, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================


"Thread-1" #11 prio=5 os_prio=0 tid=0x00000000175ba800 nid=0x232c waiting for monitor entry [0x000000001889f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadLockDemo$2.run(DeadLockDemo.java:37)
        - waiting to lock <0x00000000e0b89280> (a java.lang.String)
        - locked <0x00000000e0b892b0> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

"Thread-0" #10 prio=5 os_prio=0 tid=0x00000000175b7800 nid=0x234c waiting for monitor entry [0x000000001861f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadLockDemo$1.run(DeadLockDemo.java:18)
        - waiting to lock <0x00000000e0b892b0> (a java.lang.String)
        - locked <0x00000000e0b89280> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

两个线程相互等待,仔细看上面的waiting to lock 和locked两个对象。是相互的。造成死锁。
造成死锁的原因和解决方案

死锁:指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
造成死锁的原因是:

  1. 因为系统资源不足。
  2. 进程运行推进的顺序不合适。
  3. 资源分配不当等。

如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则
就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。

那么死锁的必要条件是:

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

这四个条件是 死锁的必要条件 ,只要系统发生死锁,这些条件必然成立,而只要上述条件之
一不满足,就不会发生死锁。

线程的状态

线程有6种状态

  1. NEW:新建,线程被构建,但是还没有start()
  2. RUNNABLE:运行,java中将就绪和运行统称为运行中
  3. BLOCKED:阻塞,线程阻塞于锁
  4. WAITING:等待,表示线程进入等待状态,需要其他线程的特定动作(通知或中断)
  5. TIMED_WAITING:带超时的等待,可以在指定的时间内自动返还
  6. TERMINATED:终止,表示线程已经执行完毕

1 多线程的优缺点_第1张图片
状态转换

线程创建之后调用start()方法开始运行。
当调用wait(),join(),LockSupport.lock()方法线程会进入到WAITING状态,而同样的wait(long timeout)sleep(long), join(long), LockSupport.parkNanos(), LockSupport.parkUtil()增加了超时等待的功能,也就是调用这些方法后线程会进入TIMED_WAITING状态,当超时等待时间到达后,线程会切换到Runable的状态,另外当WAITINGTIMED _WAITING状态时可以通过Object.notify(),Object.notifyAll()方法使线程转换到Runable状态。当线程出现资源竞争时,即等待获取锁的时候,线程会进入到BLOCKED阻塞状态,当线程获取锁时,线程进入到Runable状态。线程运行结束后,线程进入到TERMINATED状态,状态转换可以说是线程的生命周期。
注意
当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态.
而使用java.util.concurrent.lockslock进行加锁的时候线程切换的是WAITING或者TIMED_WAITING状态,因为lock会调用LockSupport的方法。

线程状态的操作

interrupted()

中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。
其他线程可以调用该线程的interrupt()方法对其进行中断操作,同时该线程可以调用 isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。
另外,同样可以调用Thread的静态方法 interrupted()对当前线程进行中断操作,该方法会清除中断标志位。

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        //sleepThread睡眠1000ms
        final Thread sleepThread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                super.run();
            }
        };
        //busyThread一直执行死循环
        Thread busyThread = new Thread() {
            @Override
            public void run() {
                while (true) ;
            }
        };
        sleepThread.start();
        busyThread.start();
        sleepThread.interrupt();
        busyThread.interrupt();
        while (sleepThread.isInterrupted()) ;
        System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
        System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
    }
}

运行结果是:


对着两个线程进行中断操作,可以看出sleepThread抛出InterruptedException后清除标志位,而busyThread就不会清除标志位。

join()

join方法可以看做是线程间协作的一种方式。
如果一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行。

public class JoinDemo {
    public static void main(String[] args) {
        Thread previousThread = Thread.currentThread();
        for (int i = 1; i <= 5; i++) {
            Thread curThread = new JoinThread(previousThread);
            curThread.start();
            previousThread = curThread;
        }
    }

    static class JoinThread extends Thread {
        private Thread thread;

        public JoinThread(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
               //join
               thread.join();
                System.out.println(thread.getName() + " terminated.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
1 多线程的优缺点_第2张图片
运行结果

如果注释了上面的thread.join();

1 多线程的优缺点_第3张图片
运行结果

每个线程都会等待前一个线程结束才会继续运行。

sleep() VS wait()

两者主要的区别:

  1. sleep()方法是Thread的静态方法,而wait是Object实例方法
  2. wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
  3. sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。

守护线程Daemon

守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。当一个Java应用,只有守护线程的时候,虚拟机就会自然退出。下面以一个简单的例子来表述Daemon线程的使用。

public class DaemonDemo {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println("i am alive");
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println("finally block");
                    }
                }
            }
        });
        //设置为守护线程
        daemonThread.setDaemon(true);
        daemonThread.start();
        //确保main线程结束前能给daemonThread能够分到时间片
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

1 多线程的优缺点_第4张图片
结果

守护线程应该先于start()方法之前。如果在之后,但是该线程还是会执行,只不过会当做正常的用户线程执行。

其他的一些概念

同步和异步

同步和异步通常用来形容一次方法调用。
同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。
而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。

并发与并行

并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。
实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。

阻塞和非阻塞

阻塞和非阻塞通常用来形容多线程间的相互影响。
比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞。
而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。

临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

你可能感兴趣的:(1 多线程的优缺点)