多线程知识点总结

一、java多线程三种创建方式
1.1 第一种,通过继承Thread类创建线程类
通过继承Thread类来创建并启动多线程的步骤如下:

1、定义一个类继承Thread类,并重写Thread类的run()方法,run()方法的方法体就是线程要完成的任务,因此把run()称为线程的执行体;

2、创建该类的实例对象,即创建了线程对象;

3、调用线程对象的start()方法来启动线程;
public class ExtendThread extends Thread {

private int i;

public static void main(String[] args) {
for(int j = 0;j < 50;j++) {

//调用Thread类的currentThread()方法获取当前线程
System.out.println(Thread.currentThread().getName() + " " + j);

if(j == 10) {
//创建并启动第一个线程
new ExtendThread().start();

//创建并启动第二个线程
new ExtendThread().start();
}
}
}

public void run() {
for(;i < 100;i++) {
//当通过继承Thread类的方式实现多线程时,可以直接使用this获取当前执行的线程
System.out.println(this.getName() + " " + i);
}
}
}

1.2 第二种,通过实现Runnable接口创建线程类
这种方式创建并启动多线程的步骤如下:

1、定义一个类实现Runnable接口;

2、创建该类的实例对象obj;

3、将obj作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;

4、调用线程对象的start()方法启动该线程;

代码实例:

public class ImpRunnable implements Runnable {

private int i;

@Override
public void run() {
for(;i < 50;i++) {
//当线程类实现Runnable接口时,要获取当前线程对象只有通过Thread.currentThread()获取
System.out.println(Thread.currentThread().getName() + " " + i);
}
}

public static void main(String[] args) {
for(int j = 0;j < 30;j++) {
System.out.println(Thread.currentThread().getName() + " " + j);
if(j == 10) {
ImpRunnable thread_target = new ImpRunnable();
//通过new Thread(target,name)的方式创建线程
new Thread(thread_target,"线程1").start();
new Thread(thread_target,"线程2").start();
}

}

}

}

1.3 第三种,通过Callable和Future接口创建线程

Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大,call()方法的功能的强大体现在:

1、call()方法可以有返回值;

2、call()方法可以声明抛出异常;

二、线程的生命周期

new Thread().start; 之后,执行完run()方法里的代码后线程就自动结束并自我销毁。无法再次调用start。只有使用了线程池,线程池可以保留核心线程不被销毁,每次来了任务可以直接使用空闲线程执行代码,不用每次都创建和销毁线程,节约资源。

三、new Ruanble().run 和new thread().start() 和的区别


new Ruanble().run :没有多线程。这个线程都在单个(现有main)线程中执行。没有线程创建,就和调用普通的方法是一样的。

R1 r1 = new R1();R2 r2 = new R2();
r1和r2类的两个不同对象实现Runnable接口,从而实现run()方法。当你调用r1.run()您正在当前线程中执行它。

new thread().start:起了独立的线程。

Thread t1 = new Thread(r1);Thread t2 = new Thread(r2);
t1和t2是类的对象。 当执行t1.start(),它启动一个新线程并调用run()方法在内部执行这个新线程。

四、Java中Synchronized的用法(简单介绍)
参考:
https://www.cnblogs.com/weibanggang/p/9470718.html


synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

修饰一个代码块:
1、一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
2、当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。
修饰一个方法:
Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){}; synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。


修饰一个静态的方法:

我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。

修饰一个类:

class ClassName {
public void method() {
synchronized(ClassName.class) {

}
}
}
锁定的是这个类的所有对象。


总结:
1、 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
2、每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
3、实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制

关于synchronized(this)中this指的是什么意思:

参考:https://www.cnblogs.com/uoar/p/7202358.html

public class SynchronizedDEmo {

public static void main(String[] args) {

TestThread tt = new TestThread();

Thread t1 = new Thread(tt);

Thread t2 = new Thread(tt);

t1.setName("t1");

t2.setName("t2");

t1.start(); t2.start();


}

public static void main2(String[] args) {

TestThread tt1 = new TestThread();
TestThread tt2 = new TestThread();

Thread t1 = new Thread(tt1);

Thread t2 = new Thread(tt2);

t1.setName("t1");

t2.setName("t2");

t1.start(); t2.start();


}

}

class TestThread implements Runnable{

private static int num = 0;

public void run() {

synchronized(this){ //此处this指的是 TestThread tt = new TestThread()对象,如果t1进来了,那么 t1获得了次对象的锁,因为t1和t2使用的是同一个对象tt,一个对象对应一把锁,他们是互斥的,t2走到此处也只能在上一句代码处等待t1获得了时间片后执行完synchronized锁住的所有代码,t2才能进去执行,若去掉synchronized(this),则t1和t2随时都可以进来执行此段代码中的任何一步,时间到了另一个接着进来执行。
//如果使用main2,tt1和tt2是两个对象,对应两把锁,则不互斥,则线程1和线程2可以同时执行这个同步代码块。

for( int i = 0; i < 20 ; i++){

num ++ ;

try {
Thread.sleep(100);
} catch (InterruptedException e) {

e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + ":" + num);

}

}

}
}


五、java并发编程:Executor、Executors、ExecutorService

可参考:https://blog.csdn.net/weixin_40304387/article/details/80508236 讲解的比较好

Executor框架是Java 5中引入的,其内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,更易管理,效率更好。Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务的线程相当于消费者,并用Runnable来表示任务,Executor的实现还提供了对生命周期的支持,以及统计信息收集,应用程序管理机制和性能监视等机制。

Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。

5.1、 Executor和ExecutorService
Executor:一个接口,其定义了一个接收Runnable对象的方法executor,其方法签名为executor(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类,一般来说,Runnable任务开辟在新线程中的使用方法为:new Thread(new RunnableTask())).start(),但在Executor中,可以使用Executor而不用显示地创建线程:executor.execute(new RunnableTask()); // 异步执行

ExecutorService:是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法,返回 Future 对象,以及可跟踪一个或多个异步任务执行状况返回Future的方法;可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。

通过 ExecutorService.submit() 方法返回的 Future 对象,可以调用isDone()方法查询Future是否已经完成。当任务完成时,它具有一个结果,你可以调用get()方法来获取该结果。你也可以不用isDone()进行检查就直接调用get()获取结果,在这种情况下,get()将阻塞,直至结果准备就绪,还可以取消任务的执行。Future 提供了 cancel() 方法用来取消执行 pending 中的任务。

5.2、Executors类: 主要用于提供线程池相关的操作

Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。

1、public static ExecutorService newFiexedThreadPool(int Threads) 创建固定数目线程的线程池。

2、public static ExecutorService newCachedThreadPool():创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。


3、public static ExecutorService newSingleThreadExecutor():创建一个单线程化的Executor。

 

4、public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

5.3、Executor VS ExecutorService VS Executors
这三者均是 Executor 框架中的一部分。 总结一下这三者间的区别,以便大家更好的理解:

1、Executor 和 ExecutorService 这两个接口主要的区别是:ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口
2、Executor 和 ExecutorService 第二个区别是:Executor 接口定义了 execute()方法用来接收一个Runnable接口的对象,而 ExecutorService 接口中的 submit()方法可以接受Runnable和Callable接口的对象。
3、Executor 和 ExecutorService 接口第三个区别是 Executor 中的 execute() 方法不返回任何结果,而 ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。
4、Executor 和 ExecutorService 接口第四个区别是除了允许客户端提交一个任务,ExecutorService 还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。
5、Executors 类提供工厂方法用来创建不同类型的线程池。比如: newSingleThreadExecutor() 创建一个只有一个线程的线程池,newFixedThreadPool(int numOfThreads)来创建固定线程数的线程池,newCachedThreadPool()可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。

5.4、自定义线程池

//创建等待队列
BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(20);
//创建线程池,池中保存的线程数为3,允许的最大线程数为5
ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,50,TimeUnit.MILLISECONDS,bqueue);

public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue)

根据ThreadPoolExecutor源码前面大段的注释,我们可以看出,当试图通过excute方法将一个Runnable任务添加到线程池中时,按照如下顺序来处理:
1、如果线程池中的线程数量少于corePoolSize,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务;
2、如果线程池中的线程数量大于等于corePoolSize,但缓冲队列workQueue未满,则将新添加的任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行);

3、如果线程池中的线程数量大于等于corePoolSize,且缓冲队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务;


4、如果线程池中的线程数量等于了maximumPoolSize,有4种处理方式(该构造方法调用了含有5个参数的构造方法,并将最后一个构造方法为RejectedExecutionHandler类型,它在处理线程溢出时有4种方式,这里不再细说,要了解的,自己可以阅读下源码)。

总结起来,也即是说,当有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize,再看缓冲队列workQueue是否满,最后看线程池中的线程数量是否大于maximumPoolSize。

另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。


下面说说几种排队的策略:

1、直接提交。缓冲队列采用 SynchronousQueue,它将任务直接交给线程处理而不保持它们。如果不存在可用于立即运行任务的线程(即线程池中的线程都在工作),则试图把任务加入缓冲队列将会失败,因此会构造一个新的线程来处理新添加的任务,并将其加入到线程池中。直接提交通常要求无界 maximumPoolSizes(Integer.MAX_VALUE) 以避免拒绝新提交的任务。newCachedThreadPool采用的便是这种策略。

2、无界队列。使用无界队列(典型的便是采用预定义容量的 LinkedBlockingQueue,理论上是该缓冲队列可以对无限多的任务排队)将导致在所有 corePoolSize 线程都工作的情况下将新任务加入到缓冲队列中。这样,创建的线程就不会超过 corePoolSize,也因此,maximumPoolSize 的值也就无效了。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列。newFixedThreadPool采用的便是这种策略。

3、有界队列。当使用有限的 maximumPoolSizes 时,有界队列(一般缓冲队列使用ArrayBlockingQueue,并制定队列的最大长度)有助于防止资源耗尽,但是可能较难调整和控制,队列大小和最大池大小需要相互折衷,需要设定合理的参数。

5.5、比较Executor和new Thread()
new Thread的弊端如下:

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

 

posted on 2021-07-20 14:02  luckyna  阅读(121)  评论(0编辑  收藏  举报

导航