Java基础(20)线程

二十.线程

进程:在操作系统上(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();提供了生产者与消费者模式,移除元素的支持

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值