【Java】 Java多线程(一)
一.对线程的理解
1.线程概念
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
2.线程的创建方式
1、继承java.lang.Thread类,并且重写它的run()方法,将线程的执行主体放在其中;
2、实现java.lang.Runnable接口,实现它的run()方法,并将线程的执行主体放在其中;
3、实现Callable接口,并实现它的call()方法实现多线程(JDK1.5)
(由于Java中类是单继承的,所以当类继承一个类时,就无法使用方式一了,开发中方式二更常用)
3.线程的状态
4.线程的同步
线程同步方式一(synchronized关键字)
可以同步方法,也可以同步代码块;对于同步方法来说,每个方法只有获取到所属类实例的锁才可以被执行,一旦该方法被执行,则独占锁,知道方法返回时或者异常退出时才会释放掉锁;同步代码块也是一样,当两个并发线程访问同一个对象中的这个synchronized(this)代码块的时候,一个时间内只有一个线程得到执行,另一个线程只有在这个线程执行完成之后才可以执行;
线程同步方式一(Lock机制)
Lock是一个接口,它是jdk1.5新增的,实现Lock接口类具有与synchronized关键字相同的功能,但功能更加强大java.utils.concurrent.locks.ReentrantLock是比较常用的;注意需要在finally中unlock释放锁;
线程阻塞
sleep()方法、yield()方法、wait()和join()等方法都可以使线程进入阻塞状态;但是yield方法和wait方法都会释放锁(cpu运行时间),而sleep方法不会释放锁。
5.线程与进程
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP(多核处理机)机器上运行,而进程则可以跨机器迁移。
二.对多线程的理解
为什么要有多线程
多线程的出现是为了提高程序的运行效率。(但并不代表多线程的程序运行效率一定高)
多线程调度
线程可以完成一定的任务,可以与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务。线程是独立运行的,它并不知道进程中是否还有其他线程存在,线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。
多线程的安全问题
多个线程在抢占执行时可能会发生安全问题 (死锁,活锁,饿锁)
1.死锁应该是最糟糕的一种情况了,它表示两个或者两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
2.活锁则是类似于: 线程A和B都需要过桥(都需要使用某个资源),而都礼让不走,就这么僵持下去的情况.
3.饿锁则类似于: 线程A想要某个资源,但后面不停的有线程拿走那个资源,从而A一直拿不到那个资源的情况
多线程产生死锁的必要条件
-
互斥条件:一个资源每次只能被一个线程使用
-
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
-
不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺
-
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
死锁案例
package test; public class DeadLock{ private static Object o1 = new Object(), o2 = new Object();//用作锁的两个对象 public static void main(String[] args) { new Thread(() -> {//lambda表达式 System.out.println("线程1开始执行"); synchronized (o1){ try{ System.out.println("线程1拿到o1锁"); Thread.sleep(1000);//线程休眠,让第二个线程有机会执行 }catch(Exception e){ e.printStackTrace(); } synchronized (o2){ System.out.println("线程1拿到o2锁执行完毕"); } } }).start(); new Thread(() -> { System.out.println("线程2开始执行"); synchronized (o2){ try{ System.out.println("线程2拿到o2锁"); Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } synchronized (o1){ System.out.println("线程2拿到o1锁执行完毕"); } } }).start(); } }
这两个线程形成死锁,谁都无法执行完毕。
避免死锁的方式
1.指定线程的执行顺序,例如,设置一个变量,每次一个执行线程完毕后变量值+1,执行下一个线程前判断变量的值。
2.“银行家”算法(资源数量 >= 线程数量*(每个线程需要的资源数量-1) + 1 时,不会出现死锁)
3.死锁检测:当一个线程获取锁的时候,会在相应的数据结构中记录下来,相同下,如果有线程请求锁,也会在相应的结构中记录下来。当一个线程请求失败时,需要遍历一下这个数据结构检查是否有死锁产生。例如:线程A请求锁住一个方法1,但是现在这个方法是线程B所有的,这时候线程A可以检查一下线程B是否已经请求了线程A当前所持有的锁,像是一个环,线程A拥有锁1,请求锁2,线程B拥有锁2,请求锁1。 当遍历这个存储结构的时候,如果发现了死锁,一个可行的办法就是释放所有的锁,回退,并且等待一段时间后再次尝试。
三.线程池的理解
线程池出现的原因
线程的频繁创建和销毁是很影响性能的一件事情。而使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。同时,根据系统的承受能力,调整线程池中工作线程的数量,防止浪费内存。
线程池类
java.util.concurrent.ThreadPoolExecutor 类就是一个线程池。客户端调用 ThreadPoolExecutor.submit(Runnable task) 提交任务,线程池内部维护的工作者线程的数量就是该线程池的线程池大小,有 3 种形态:
当前线程池大小 :表示线程池中实际工作者线程的数量;
最大线程池大小 (maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限;
核心线程大小 (corePoolSize ):表示一个不大于最大线程池大小的工作者线程数量上限。(如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队;如果运行的线程等于或者多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不是添加新线程;如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出 maxinumPoolSize, 在这种情况下,任务将被拒绝)
线程池的使用
1.创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler);//参数根据情况设置
2.使用线程池中的线程
pool.execute(new Runnable(){ @override public void run(){ //code } });