java基本篇--多线程


一、线程基本属性

1、线程的6种状态

NEW(新建):还没有调用start()开启线程实例所处的状态
RUNNABLE(运行):正在虚拟机中执行或者等待被执行的线程所处的状态,但是这种状态也包含线程正在等待处理器资源这种情况
BLOCKED(阻塞):等待在监视器锁上的线程所处的状态,比如进入synchronized同步代码块或同步方法失败
WAITING(等待):等待其它线程执行特定操作的线程所处的状态;
TIMED_WAITING(超时等待):等待其它线程执行超时操作的线程所处的状态
TREMINATED(结束):退出线程所处的状态

二、创建线程的4种基本方式

1、继承Thread类

重写run方法
优点:实现简单,只需要实例化继承类的实例,即可使用线程
缺点:扩展性不足,如果一个类已经继承其它的类,就无法通过这种方式自定义线程。

public class Mythread extends Thread{
	@Override
	public 	void run(){}
}
public class test{
	Thread thread = new Thread();
	thread.start();
}

2、实现Runnable接口

优点:
1.扩展性好,可以在此基础上继承其它类,实现其它必须的功能
2.对于多线程共享资源的场景,具有天然的支持,适用于多线程处理一份资源的场景
缺点:构造线程实例的过程相对繁琐一点

public class MyRunnable implements Runnable{
	@Override
	public void run(){
		Sysout.out.print("线程1");
	}
	public static void main(String[] args){
		//线程执行的目标对象
		MyRunnable myRunnable = new MyRunnable();
		//实际的线程对象
		Thread thread  = new Thread(myRunnable);
		//启动线程
		thread.start();
	}
}

3、实现Callable接口

优点:
1.扩展性好
2.支持多线程处理同一份资源
3.具备返回值以及可以抛出受检查的异常
缺点:
相对于实现Runnable接口的实现方式,较为繁琐

public class MyCallable implements Callable<String>{
	@Override
	public String call() throws Exception{
		return "这是有一个线程";
	}
	public static void main(String[] args){
		//线程执行目标
		MyCallable myCallable = new MyCallable();
		//包装线程的执行目标,因为Thread的构造函数只能接受Runnable接口的实现类,而FutureTask类实现了Runnable接口
		FutureTask<String> futureTask = new FutureTask<>(myCallable);
		//传入线程执行目标,实例化线程对象
		Thread thread = new Thread(futureTask);
		//启动线程
		thread.start();
		String result = null;
		try{
		}catch(InterruptedException e){
			e.printStackTrace();
		}catch(ExecutionException e){
			e.printStackTrace();
		}
		Sysout.out.println(result);
	}

}

4、内部类的方式

优点:
1.相比于前3中方法代码更加简洁,使用更加方便
缺点:
1.每次new Thread新建对象性能差
2.线程缺法统一管理,可能无限制创建新的线程,相互竞争,很有可能会占用过多的系统资源导致死机或oom
3.缺乏更多的功能如定期执行、定时执行、线程中断

//1.匿名内部类实现方式
new Thread(){
	//重写run方法
	@Override
	public void run(){
		Sysout.out.print("线程执行中");
	}
}.start();

//2.线程接口runnable实现方式
Runnable r = new Runnable(){
	Person p = new Person("某人");
	@Override
	public void run(){
		Sysout.out.print("线程执行中");
	}
}
new Thread(r).start();

//3.简化Runnable接口的实现方式
new Thread(new Runnable(){
	@Overable
	public void run(){
		Sysout.out.print("线程执行");
	}
}).start();

5、小结

1、采用实现Runnable、Callable接口的方式创建多线程
优势:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
2、使用继承Thread类的方式创建多线程
优势:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势: 线程类已经继承了Thread类,所以不能再继承其他父类。
3、Runnable和Callable的区别
(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3) call方法可以抛出异常,run方法不可以。
(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果future.get()。

三、4种线程池

1、Java通过Executors提供四种线程池

线程池
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

2、创建线程池

(1)newCachedThreadPool:

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。示例代码如下:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
  for (int i = 0; i < 10; i++) {
    final int index = i;
  try {
    Thread.sleep(index * 1000);
   } catch (InterruptedException e) {
      e.printStackTrace();
  }
   cachedThreadPool.execute(new Runnable() {
     @Override
     public void run() {
        log.info(index);
      }
   });
}

(2)newFixedThreadPool:

需要指定线程池的大小,创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache示例代码如下:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
 for (int i = 0; i < 10; i++) {
 final int index = i;
 fixedThreadPool.execute(new Runnable() {
 @Override
 public void run() {
     try {
        log.info(index);
        Thread.sleep(2000);
     } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
   }
 });
}

(3)newScheduledThreadPool:

创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
 scheduledThreadPool.schedule(new Runnable() {
      @Override 
      public void run() {
         log.info("delay 3 seconds");
       }
 }, 3, TimeUnit.SECONDS);

表示延迟3秒执行。
定期执行示例代码如下:

scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
          log.info("delay 1 seconds, and excute every 3 seconds");
      }
}, 1, 3, TimeUnit.SECONDS);

表示延迟1秒后每3秒执行一次。
ScheduledExecutorService比Timer更安全,功能更强大

(4)newSingleThreadExecutor:

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
  final int index = i;
  singleThreadExecutor.execute(new Runnable() {
    @Override
    public void run() {
    try {
        log.info(index);
        Thread.sleep(2000);
     } catch (InterruptedException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
     }
    }
  });
}

结果依次输出,相当于顺序执行各个任务。

3、线程池的作用

程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

4、为什么要用线程池

(1)减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
(2)可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
(3)Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的
线程池接口是ExecutorService。

5、比较重要的几个类:

ExecutorService: 真正的线程池接口。
ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutor: ExecutorService的默认实现。
ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

6、使用线程池和普通创建线程的区别

a. 每次new Thread新建对象性能差。
b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom(out of memory)。
c. 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,线程池的好处在于:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。

7、线程池参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

(1)corePoolSize:核心线程数
线程池中的常驻核心线程数,当一个任务提交到线程池时,线程池会创建一个线程来执行任务,即使空闲的线程数能够执行新任务也会创建新的的线程,直到执行任务数大于核心线程数时就不会再创建。如果调用线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程;
(2)maximumPoolSize:最大线程数
线程池允许创建的最大线程数,此值大于等于1;可以创建出超过核心线程数量的线程,在keepAliveTime空闲时间时终止;
(3)keepAliveTime:空闲时间
多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁 ,直到剩下corePoolSize个线程为止。默认情况下,只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,知道线程中的线程数不大于corepoolSIze,也可以手动设置到核心线程上;
(4)TimeUnit:时间单位
keepAliveTime的单位;
(5)runnableTaskQueue:任务队列
任务队列,被提交但尚未被执行的任务
(6)ThreadFactory:设置创建线程的工厂
表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可;
(7)Handler:拒绝策略
表示当线程队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略

Abort策略:
默认策略,新任务提交时直接抛出未检查的异常RejectedExecutionException,该异常可由调用者捕获。
CallerRuns策略:
为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程中运行新的任务。
Discard策略:
新提交的任务被抛弃。

JDK内置的拒绝策略

当线程池中corePoolSize使用完毕之后,多出来的任务就会进入缓存队列;
当缓存队列也存放满了之后,还有大量的任务,这时将会创建线程池中支持的最大量的线程,然后执行缓存队列里面的任务,而外面的任务进入缓存队列中;当线程池中的线程达到最大时,缓存队列也存满之后,这时该线程池将会执行拒绝策略。

8、小结

线程池的优点:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。

四、线程并发

并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。
并行:是说在单位时间内多个任务同时在执行 。
在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。

五、线程锁

(1)枷锁目的

解决并发导致的共享资源错乱,避免多个线程同时对一个共享资源进行操作,保证公共资源的唯一性、正确性和真实性,使得同一时段内只有一个线程对公共资源进行操作;

(2)锁的种类

互斥锁:加锁失败后,线程会释放cpu,给其它线程,性能损耗大;
自旋锁:加锁失败后,线程会忙等待,直到它拿到锁,性能损耗小;
读写锁:写锁是独占锁,因为任何时刻只能有一个现场持有写锁,类似互斥锁和自旋锁;
悲观锁:多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁;
乐观锁:先修改后检查,修改后检查到没有冲突则操作完成,否则操作放弃,乐观锁全程并没有加锁,所以也叫做无锁线程,虽没有加锁但是一旦发生冲突,重试成本非常高;
注释:不管哪种锁,加锁的代码范围应该尽可能的小,减小颗粒度,提高执行速度;

(3)常用的加锁方式

synchronized关键字


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值