二十.线程 |
进程:在操作系统上(os),一个独立运行的任务被称为进程,进程是可以并发执行的(即多个进程可以同时运行) 线程是进程中,多个并发执行的任务逻辑,线程是进程的组成单位,一个进程至少要有一个线程, 原因是:进程的任务实际的执行者是线程 类比 进程——小组 线程——小组成员 任务是分配给小组的,但实际执行 小组任务的是小组成员 一个进程的任务实际上是由(1——n)个线程来完成的,对于java来说,JVM相当于操作系 统上运行的进程,JVM一定会 包含一个线程被称为主线程,而main函数就是由主线程来执行的 多线程在宏观上是并行,微观上是串行 |
一.线程的组成 |
1.CPU: 一个线程进行执行时,需要使用CPU CPU具有时间片,哪个线程获得时间片哪个线程去使用CPU 使用CPU的时间由时间片决定,时间片的分发和管理由操作系统负责 2.数据: 每个线程拥有自己的JVM栈 栈空间独立:每个线程执行逻辑中方法的调用都是独立的 所有线程共享用一个堆空间 堆空间共享:每个线程中产生的数据如果是产生在堆空间中的,那么都是共享的 比如创建的对象 |
二.线程的创建 非常重要 |
对于其他线程的创建于开启,一般要依赖于某个线程(主线程)所以创建线程开启线程的代码,往往要放置在主函中 1.先创建任务对象,再创建线程对象 a.定义任务类 Runnable接口的实现类就是任务类 class MyTask implements Runnable{ public void run(){ for(char c='A';c<='Z';c++){ System.out.println(c); } } } b.创建任务对象 MyTask task1=new MyTask(); 前两步操作也可以使用Lambda与匿名内部类来完成 c.创建线程对象 ,并将任务提交给线程 Thread t1 = new Thread(task1); d.开启线程,调用线程的start方法 t1.start(); 2.创建一个自带任务的线程 a.自定义一个线程类,写一个类,继承Thread 重写父类的run方法 run方法中写的就是该自定义线程类自带的任务 class MyThread extends Thread{ public void run(){ for(char c='A';c<='Z';c++){ System.out.println(c); } } } b.创建自定义线程类对象 并开启线程 MyThread t1 = new MyThread(); t1.start(); 这两步可以使用匿名内部类直接搞定 但是不能用Lambda |
三.线程的状态 面试常问 |
1.线程的官方状态 (1)NEW 创建一个线程 而没有启动时 (2)RUNNABLE 可以获得时间片 或 正在执行时 这两种状态都是RUNNABLE可运行状态 (3)BLOCKED 阻塞状态:进入到阻塞状态的线程会放弃时间片,且不再参与时间片的争夺 (4)WAITING 无限期等待,当线程被其他线程加队时(执行了其他线程对象的join方法时)会进入到该状态 (5)TIMED_WAITING 有限期等待 当线程执行了 Thread.sleep方法时,会让当前线程进入到有限期等待 (6)TERMINATED 当线程任务执行完毕时 进入到终止状态 2.改变线程状态的方法 a.Thread.sleep(毫秒数);必须掌握 如果某个线程在执行时,执行了该方法 那么该线程就会进入到睡眠,放弃自己的时间片, 在有限时间中,不参与时间片的争夺,不会放弃锁标记 b.线程对象.join(); 可以不会 在一个线程任务中,让某个线程对象调用join时,会让调用该方法的线程加队到当前线程之前,当前线程进入 到无限期等待,并放弃时间片,不参与时间片的获取 例:比如线程A的任务中,执行了B.join()此时A会主动放弃时间片,不参与到时间片的争夺,进入到无限期等待,什么时候B的 任务执行完,什么时候A从无限期等待恢复到RUNNABLE状态 |
四.Java中的线程池 |
线程池:线程池存放了一定数量的线程,这些线程可以重复的被利用,不必 频繁的创建与销毁 好处:不用频繁的创建与销毁线程,节省系统资源,提高效率 1.线程池的类型 a.线程池的顶级接口:Executor b.线程池的常用类型:ExecutorService c.线程池官方实现类:ThreadPoolExecutor 不推荐使用该类创建线程池实例,如果有公司要求按照公司规范进行创建 线程池构造的7个参数,需要在出去面试前 背会 2.创建线程池对象 a.获取java中接口类型的对象 (1)使用官方提供的实现类创建对象 (2)自己书写实现类创建对象 (3)调用官方的工厂方法,直接获取接口类型的对象,而不去自己手动创建 b.什么是工厂方法? 工厂方法的实现,是使用某种手段创建一个接口类型的对象,该方法会给调用者返回一个接口类型实现类的对象, 而让调用者忽略创建实现类对象的过程 c.获取线程池对象的两个常用线程池方法 线程池相关的工厂方法,都被放置在Executors的工具类中 static ExecutorService new CachedThreadPool() 会返回一个缓存机制的线程池:线程池在创建时不包含任何的线程,当有一个任务提交时,会验证是否有闲置的 线程,如果有就把任务给闲置线程,如果没有将新创建一个线程接受任务,一个线程在完成任务后,会等待60秒 ,当60秒等待期间没有新任务时,线程会被销毁,不会有任何任务等待 static ExecutorService newFixedThreadPool(int nThreads) 会返回固定线程数量的线程池:参数就是规定线程池在创建时,会创建几个线程,当有新任务提交时,会先查看是否有闲置线程 ,如果有就提交给闲置线程,如果没有不会创建新的线程,而是任务等待闲置线程,存在任务等待的情况 3.如何给线程池提交任务 (1)创建一个线程池 (2)使用线程池的submit方法 提交任务(提交任务对象) ExecutorService pool1=Executors.newCachedThreadPool(); Runnable task1=()->{ for(int i=1;i<26;i++){ System.out.println(i); } }; Runnable task2=()->{ for(ichar c='A';c<'Z';c++){ System.out.println(c); } }; pool1.submit(task1); pool1.submit(task2); 4.Callable接口 了解 Runnable接口的run方法 存在问题 不能给任务的发布者返回计算的结果,也不能抛出异常 JDK1.5推出了新的任务类型 Callable类型 Callable类型只能与线程池搭配不能与手动创建的线程搭配使用 创建Callable类型的任务类 class 类名 implements Callable<泛型>{ public 泛型 call()throws 异常....{ return 给任务发布者返回的数据; } } a.如何获取Callable类型任务的 任务结果? (1)创建Callable类型的任务类 class MyTask2 implements Callable<Integer>{ public Integer call() throws Exception{ int sum=0; for(int i=1;i<=500;i++){ sum+=i; } return sum; } } class MyTask3 implements Callable<Integer>{ public Integer call() throws Exception{ int sum=0; for(int i=501;i<=1000;i++){ sum+=i; } return sum; } } (2)创建Callable类型的任务对象 MyTask2 task2=new MyTask2(); MyTask3 task3=new MyTask3(); (3)将Callable类型的任务对象提交给线程池 ExecutorService pool=Executors.newCachedThreadPool(); (4)将任务提交给线程后,会得到Future的未来对象,因为计算结果产生在未来 Future<Integer> f1=pool.submit(task2); Future<Integer> f2=pool.submit(task3); (5)通过Future的get方法可以获取Callable类型的任务对象的计算结果 System.out.println("主线程在干其他事情。。"); System.out.println("主线程需要其他线程的计算结果,等待中..."); Integer integer = f1.get(); Integer integer2 = f2.get(); Integer result = integer+integer2; System.out.println(result); 注意:当Callable类型的任务对象还未将结果计算完成时,未来对象的get方法会使得当前线程进入阻塞状态,等到获取到 结果是,才能继续执行 |
五.线程安全问题 |
1.名词解释 必须记住 非常重要 a.临界资源:在多线程并发下,多个线程共享的某个数据,被称为临界资源 b.原子操作:多步操作被视为一个整体,在执行顺序上不可以被打破 c.线程不同步/不安全:在多线程并发下,原子操作被破坏,临界资源数据出现问题 d.线程的同步/安全:在多线程并发下,保证原子操作不被破坏,从而保证临界资源的数据安全 e.死锁:两个线程相互等待对方释放所有占据的互斥锁标记,从而使得两个线程都会进入阻塞状态, 使得程序无法向下执行 2.如何保证线程的同步 重要 Java的每一个对象都会有一个 互斥锁标记 a.使用同步代码块保证线程同步 synchronized(临界资源对象){ //原子操作 } 当一个线程,是第一个访问访问同步代码块的线程,此时该线程获取到临界资源的互斥锁标记, 当原子操作执行完毕时,会归还互斥锁标记 当一个线程想要执行原子操作,会先试图获取互斥锁标记,获取成功则执行原子操作,如果获取失败 则进入阻塞状态 阻塞状态的线程会放弃时间片,不参与时间片的争夺 当一个线程释放了互斥锁标记时,JVM会通知所有处在阻塞状态并等待该互斥锁标记的所有线程, 此时这些线程会等待互斥锁标记的随机分配,哪个线程获取到该互斥锁标记,哪个线程回归到RUNNABLE 状态,参与时间片争夺 b.使用同步方法来保证线程的同步 如果一个方法的内容全部都是原子操作,并且临界资源使用的是当前对象,那么我们可以直接把该方法声明为 一个同步方法 语法: 修饰符 synchronized 方法返回值 方法名(参数表){ //原子操作 } 例: public void push (String s){ synchronized(this){ str[size]=s; try{ Thread.sleep(1000); }catch(InterruptedException e){ e.printStackTrace(); } size++; } } 转为同步方法: public synchronized void push (String s){ str[size]=s; try{ Thread.sleep(1000); }catch(InterruptedException e){ e.printStackTrace(); } size++; } 关于同步代码块与同步方法的使用位置: 同步代码块,一般使用在任务代码块中 同步方法,往往使用在临界资源的方法上 3.线程的状态图 4.对于集合是否线程安全,可变长字符串是否线程安全的解读 线程安全的实现类:往往内部方法都有同步操作 线程不安全的实现类:往往内部的方法没有做线程的同步操作 例如: Vector为什么线程安全?ArrayList为什么线程不安全? Vector中的方法都是同步方法,被synchronized修饰 ArrayList所有方法都不是同步方法 5.线程间通讯 不是很重要 void wait() 当线程执行到 临界资源.wait()时,当前线程会立刻放弃时间片,放弃所有的 互斥锁标记,进入到无限期等待 void notify() 当线程执行到 临界资源.notify(),JVM会去随机唤醒一个处于无限期等待且 正在等待该临界资源的线程,唤醒后该线程会从 无限期等待进入到阻塞状态 从而等待JVM通知临界资源被释放然后准备争抢 void notifyAll() 当线程执行到 临界资源.notifyAll(),JVM会去唤醒所有处于无限期等待且 正在等待该临界资源的线程,唤醒后该线程会从 无限期等待进入到阻塞状态 从而等待JVM通知临界资源被释放然后准备争抢 6.lock锁 同步代码块以临界资源的互斥锁标记作为占据临界资源的标志 lock锁以自身为一个标记,先进行加锁的线程就占据lock锁,当解锁时释放lock锁 使用lock锁的步骤: (1)创建一个lock锁对象——使用实现类ReentrantLock获取lock锁对象 Lock lock = new ReentrantLock(); (2)对原子操作的开始进行加锁,在原子操作结束后进行解锁 public void push (String s){ synchronized(this){ str[size]=s; try{ Thread.sleep(1000); }catch(InterruptedException e){ e.printStackTrace(); } size++; } } 使用lock锁替换 public void push (String s){ lock.lock(); str[size]=s; try{ Thread.sleep(1000); }catch(InterruptedException e){ e.printStackTrace(); } size++; lock.unlock(); } lock锁常用方法: (1)lock()上锁 (2)unlock()解锁 (3)tryLock()引用加锁,尝试获取lock对象如果lock被占据返回false 如果lock对象没有被占据则返回true并等效于lock() lock锁的好处:使得锁具有更具体的表现,代码可读性高,可以提高代码的灵活度,提高程序效率 |
六.线程安全的集合 |
1.将List、Set、Map集合转换为线程安全的集合 可以利用Collections工具类型中的一些方法 进行转换 a.将一个线程不安全的Set转换为安全的Set static <T> Set<T> synchronizedSet(Set<T> s) b.将一个线程不安全的List转换为安全的List static <T> List<T> synchronizedSet(List<T> list) c.将一个线程不安全的Map转换为安全的Map static <K,V> Map<kK,V> synchronizedSet(Mao<K,V> m) 例: ArrayList<String> list=new ArrayList<String>(); List<String> synchronizedList= Collections.synchronizedList(list); HashMap<String,String> map=new HashMap<>(); Map<String,String> synchronizedMap=Collections,synchronizedMap(map); HashSet<String> set=new HashSet<>(); Set<String> synchronizedSet=Collections.synchronizedSet(Set); 2.JDK1.5之后推出的关于List与Map集合的新型实现类 (1)List的新型实现类 CopyOnWriteArrayList:线程安全且还能保证List的性能 对集合进行 增删改查时 会进行加锁,并且在进行 增删改时会对底层数组做一个备份,增删改操作都会去操作这个备份数组,而不是操作原数组,等增删改 执行完毕时,将数组更新,而无论在任何时候读都不会加锁,每次读的都是原数组 因为读不加锁,所以进行读时效率特别高,如果一个List集合读操作远远大于写操作,建议采用CopyOnWriteArrayList (2)Map集合的新型实现类 ConcurrentHashMap:线程安全且高效的新型实现类,不可以使用null作为键或值 JDK1.7的实现:使用Segments+EntryArray来实 现 采用分段锁,对每个Segment进行加锁,默认为 16个Segments,存储元素时先计算在Segments+EntryArrayList的实现方式,采用CAS+synchronized 来实现更高效的安全保障,采用原生的HashMap散列结构,对hash桶操作时会对整个hash桶进行加锁 ,如果是插入操作则使用CAS算法高效插入,结构类似于分段锁 3.JDK1.5之后推出的新型集合Queue (1)Queue中元素的特性:先进先出,后进后出 (2)常用方法: 添加: add(); offer(); 获取: peek();每次获取队列头的元素,不让元素出队列,当队列头为空时返回null poll();每次让一个队列的元素出队列,并获取该元素,当队列头为空时返回null (3)常用实现类 a.LinkedList——最基本的实现类,对于队列没有长度限制 b.ConcurrentLinkedQueue——线程安全,支持并发,使用CAS来邦正数据的安全性 效率极高 c.BlockingQueue接口下的实现类: LinkedBlockingQueue:底层是链表实现 ArrayBlockingQueue:底层数组实现 BlockingQueue接口规定了该接口下的实现类必须是阻塞队列,必须实现生产者与消费者模式 put();提供了生产者与消费者模式,添加元素的支持 take();提供了生产者与消费者模式,移除元素的支持 |
Java基础(20)线程
最新推荐文章于 2023-04-16 12:27:45 发布