线程
1 线程概述
1 线程相关基本概念:程序,进程,线程
1)程序:为了完成特定的任务,用某种语言编写的一组指令的集合,即一段静态的代码,静态对象。
2)进程:是程序的一次执行过程,或者是正在运行中的程序。是一个动态过程==>生命周期。
- 程序是静态的,进程是动态的。
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
3)线程:是对进程的细分,是一个程序内部的一条执行路径。
-
若一个进程同一时间并行执行多个线程,就是支持多线程的。
-
线程作为调度和执行的单位,每个线程拥有独立的栈和程序计数器(pc),线程切换的开销小。
-
一个进程中的多个线程共享相同的内存单元/内存地址空间–>他们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间的通信更简便,高效。
- 多个线程操作共享的系统资源可能带来安全隐患==>线程的同步。
-
多个线程程序,每个线程拥有一份独立的虚拟机栈和程序计数器。多个线程共享一份方法区和堆。
拓展:jvm结构。
2 并行和并发
-
并行:多个cpu同时执行多个任务。比如:多个人同时做不同的事情。
-
并发:一个cpu(采用时间片)同时执行多个任务。比如:秒杀,多个人做通过一件事情。
注意:一个java应用程序运行的过程中至少存在三个线程:main()主线程,GC()垃圾回收线程,异常处理线程。
3 多线程的优点以及用途
1)优点:
- 提高应用程序的响应,对图像化界面更有意义,可增强用户的体验。
- 提高计算机系统cpu的利用率。
- 改善程序结构。将长而复杂的进程分为多个线程,独立运行,利于理解和修改。
2)用途:
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户的输入,文件读写操作,网络操作,搜索等。
- 需要一些后台运行的程序时(守护线程)。
2 多线程的创建和使用
2.1 多线程的创建
2.1.1 继承Thread类
1 实现步骤:
- 创建一个继承于Thread类的子类
- 重写Thread类的run()方法–>此线程执行的操作声明到run方法中。
- 创建Thread类的子类对象。
- 通过此对象调用start()。
代码实现:
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//start():启动当前线程;jvm调用此线程的run()。
//如果我们调用run(),则此时不是多线程,整个操作都是main线程执行的==>启动线程需要使用start()。
myThread.start();
//不可以让已经start的线程再次执行start()的方式创建新的线程:IllegalThreadStateException()
//myThread.start();
MyThread myThread1 = new MyThread();
myThread1.start();
//此操作在main线程中执行的。
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i+"**********main*******");
}
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+"-"+i);
}
}
}
}
匿名子类的方式创建多线程
new Thread(){
@Override
public void run() {
super.run();
}
}.start();
2.2.1 实现runnable接口
1 实现步骤:
- 实现runnable接口的类
- 实现run()。
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread对象。
- 通过Thread类的对象调用start()。
2 底层逻辑:
1)创建Thread对象
public Thread(Runnable target) {
//初始化target属性
init(null, target, "Thread-" + nextThreadNum(), 0);
}
//What will be run.
//private Runnable target;
2)thread对象调用start()==>start0();
然后调用线程的run()方法,因为target!=0,所以调用target的run()方法。
@Override
public void run() {
if (target != null) {
target.run();
}
}
3 简单使用
public class RunnableTest{
public static void main(String[] args) {
MyRunnable myRunnable=new MyRunnable();
Thread thread=new Thread(myRunnable);
thread.start();
Thread thread1=new Thread(myRunnable);
thread1.start();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
4 问题:
1 )我们能不能直接通过run()的方式来启动线程?
不可以,使用run()的方式启动的不是多线程只是单纯的方法的调用。
2 )使用start()启动已经start()的线程来创建新的线程可以吗?
不可以,会报:IllegalThreadStateException();
需要重新创建一个对象,然后调用start()方法的方式来实现新的线程的创建。
3)创建线程的两种方式的比较。
①继承Thread:
类的继承存在局限性,只可以继承一个类
②实现Runnable(优先选择):
-
实现的方式更适合来处理多个线程有共享数据的情况,天然的可以实现属性的共享。
-
没有类的单继承的局限性。
联系:Thread类实现了Runnable接口
相同点:都需要实现Runnable接口中的run()方法。
4)idea中顶级结构是个工程,想要多开工程只能重开一个窗口。
idea中的project相当于eclipse中的workspace;
idea中的module相当于eclipse中的project。
2.2 线程中常用的方法
- start():启动当前线程;调用当前线程的run()。
- run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在该方法内。
- currentThread():静态方法,返回执行当前代码的线程。
- getName():获取当前线程的名字。
- setName():给当前线程命名。还可以使用构造器来进行命名。
- yield():释放掉的当前cpu的执行权。当然有可能再次分给该线程。
- join():在线程A中调用线程B的join方法,线程A就进入阻塞状态,直到线程B完全执行完之后,A才结束阻塞状态。然后等待cpu来分配资源。
- stop():已时的,不建议使用。执行此方法时,强制结束当前线程。
- sleep():静态的方法:将当前线程睡眠指定的时间,单位是ms。使用之后线程是阻塞的状态。
- isAlive():返回值boolean:判断当前方法是否还存活。
public class ThreadMethodTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("张三线程");
myThread.start();
Thread.currentThread().setName("李四");
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
if (i==20){
try {
myThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(myThread.isAlive());
System.out.println(Thread.currentThread().getName()+":"+Thread.currentThread().getPriority());
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+Thread.currentThread().getPriority());
for (int i = 0; i < 100; i++) {
if (i%2==0){
try {
//只能try-catch:父类方法没有抛出异常。
sleep(10 );
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
if (i%20==0){
yield();
}
}
}
}
2.3 线程的调度
1 调度的策略
- 时间片:一个线程执行固定的时间片时间。
- 抢占式:高优先级的线程抢占cpu的几率高
2 Java的调度方法
- 同优先级的线程组成先进先出队列(先到先服务),使用时间片策略。
- 对于高优先级,使用优先调度的抢占式策略。
3 线程的优先级==priority
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
4 getPriority():返回线程优先值。
setPriority(int newPriority):设置线程的优先级。
注意:优先级高低,只是抢占到cpu资源的概率高,并不是优先级高的一定比优先级低的先执行,也不是高优先级的先执行完,低优先级的再执行。
5 线程的分类
-
守护线程:用来辅助用户线程的,依赖于用户线程==>如果JVM中全是守护线程,当前JVM就会退出。可以在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成守护线程。守护线程举例:例如:GC()垃圾回收线程。
-
用户线程:相当于主线程。
2.4 线程的生命周期
Thread.State枚举类的源码:
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
精简的5种状态
-
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建的状态。
-
就绪:当处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时他已经具备运行的条件,只是没有分配到CPU资源
-
运行:当就绪的线程被调度并获得CPU资源的时候,便进入到运行状态,run()方法定义了线程的操作和功能。
-
阻塞:在某种特殊的情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
-
死亡:线程完成他的全部工作被提前强制性的中止或出现异常导致结束。
某个方法的调用引起了状态的切换;
状态的变化导致某个方法的调用==>回调函数。
2.5 线程的安全
1 线程安全问题的提出
- 多个线程执行的不确定性引起执行结果的不稳定。
- 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
例如:储蓄卡变成信用卡
2 例题示意
1)案例说明:
-
Runnable的方式实现卖票:
-
问题:存在线程安全的问题:会出现错票,同号的票。
-
出现问题的原因:当某个线程操作车票的过程中,尚未操作完成,其他线程参与进来,也操作车票。
-
如何解决:当一个线程在操作共享数据的时候,其他线程不能参与进来,直到线程A操作完时,其他线程才可以参与进来。这种情况下,即便线程A出现了阻塞也不能改变。Java中,我们可以通过同步机制来解决现成的安全问题。
方式一:同步代码块:synchronized(同步监视器){被同步的代码(操作共享数据【被多个线程共享的变量】的代码==不能过多,也不能过少,比如多了while就成了单线程了。只能有一个对象可以实现卖票) }
同步监视器:俗称锁,任何一个类的对象,都可以充当锁【多个线程必须要公用同一个锁==>同一个对象】。
方式二:同步方法:synchronized修饰方法。
同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
非静态的同步方法,同步监视器是:this(实现Runnable的方式)
静态的同步方法,同步监视器是:当前类本身。
2)同步的方式
优点:解决了线程的安全问题。
缺点:操作同步代码时,只能有一个线程参与,其他线程只能等待,相当于单线程的操作,效率相对比较低。synchronized可以来保证线程安全。
public class WindowForRunnableTest {
public static void main(String[] args) {
WindowForRunnable window = new WindowForRunnable();
Thread window1 = new Thread(window);
window1.setName("window1");
Thread window2 = new Thread(window);
window2.setName("window2");
Thread window3 = new Thread(window);
window3.setName("window3");
window1.start();
window2.start();
window3.start();
}
}
class WindowForRunnable implements Runnable{
//指向一个对象,所以不需要加static来修饰
private int ticket=100;
Object object=new Object();
@Override
public void run() {
//不适合直接对run方法使用synchronized来修饰,会变成单线程==>单窗口卖票。
//将共享数据封装到一个方法内,使用synchronized来修饰。
while(true){
getTicket();
}
}
public synchronized void getTicket(){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖第"+ticket+"张票");
ticket--;
}
}
/*@Override
public void run() {
while(true){
//任何一个对象都可以充当锁,或者同步监视器。
//对于此类中可以考虑监视器为this,使用当前对象就可以。
synchronized (object){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖第"+ticket+"张票");
ticket--;
}else{
break;
}
}
}*/
}
2.6 线程死锁
1 线程死锁的概述
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程死锁。出现死锁之后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
2 解决的方式
- 专门的算法,原则。
- 尽量减少同步资源的定义。
- 尽量避免嵌套同步。
3 线程死锁代码的演示:
- 第一个线程手握s1,想要s2。
- 第二个线程手握s2,想要s1。
4 代码示例
public class ThreadSafeTest {
public static void main(String[] args) {
StringBuffer s1=new StringBuffer();
StringBuffer s2=new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
5 死锁演示
package com.atguigu.java;
//死锁的演示
class A {
public synchronized void foo(B b) { //同步监视器:A类的对象:a
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法"); // ①
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); // ③
b.last();
}
public synchronized void last() {//同步监视器:A类的对象:a
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {//同步监视器:b
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法"); // ②
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); // ④
a.last();
}
public synchronized void last() {//同步监视器:b
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
@Override
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
2.7 线程同步
1 LOCK锁:解决线程安全的第三种方式
- JDK5.0开始,java提供了更为强大的线程同步机制–显式定义同步锁对象来实现同步。同步锁使用LOCK对象充当。Java.util.concurrent.locks.Lock接口时控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。
- ReentrantLock类实现了Lock,他拥有和synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁和释放锁。
2 代码演示
package com.atguigu.exer2;
import java.util.concurrent.locks.ReentrantLock;
/**
* 银行有一个账户:两个储户分别网同一个账户存3000,每次存1000,存三次。每次存完打印账户余额。
* @author 龍
*/
public class AccountTest {
public static void main(String[] args) {
/*Account account=new Account();
Thread thread01=new Thread(account);
thread01.start();
Thread thread02=new Thread(account);
thread02.start();*/
System.out.println("-------------------------");
Account1 account1=new Account1(0);
Custumer custumer1=new Custumer(account1);
custumer1.setName("甲");
Custumer custumer2=new Custumer(account1);
custumer2.setName("乙");
custumer1.start();
custumer2.start();
}
}
class Account implements Runnable{
private int money=0;
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
lock.lock();
Thread.sleep(100);
money+=1000;
System.out.println("当前余额为:"+money);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
class Account1{
private double money;
public Account1(double money){
this.money=money;
}
public void deposit(double amt){
if (amt>0){
money+=amt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"存钱成功!!。余额为:"+money);
}
}
}
class Custumer extends Thread{
private Account1 account;
//使用继承的方式需要对lock使用静态来修饰。
private static ReentrantLock lock=new ReentrantLock();
public Custumer(Account1 account){
this.account=account;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try{
lock.lock();
account.deposit(1000);
}finally {
lock.unlock();
}
}
}
}
2.8 线程通信
1 线程通信涉及到三个方法
- wait():一旦执行此方法,当前线程就会进入阻塞状态,释放同步监视器。
- notify():一旦执行该方法,就会唤醒被wait的一个线程,如果存在多个线程,唤醒优先级高的那个。
- notifyAll():一旦执行该方法,就会唤醒所有被wait的线程。
说明:
- wait(),notify(),noifyAll()只适用于同步代码块或者同步方法的线程之间的通信。
- wait(),notify(),noifyAll()三个方法的调用者必须是由同步方法或者同步代码块的同步监视器来调用,否则会出现java.lang.IllegalMonitorStateException
2 这三个方法是定义在java.lang.Object类中。
面试题:sleep()和wait()的异同。
相同点:1)一旦执行到这两个方法,都可以使得当前的线程进入阻塞状态。
不同点:1)两个方法声明的位置不一样,sleep()在Thread中,wait()在Object中。
2)调用的要求不一样:sleep()可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中。
3)关于是否释放同步监视器中:都使用在同步代码块或者同步方法中,sleep()不会释放锁,wait会释放(同步监视器)。
3 代码演示
public class CommunicationTest {
public static void main(String[] args) {
Number number=new Number();
Thread thread01=new Thread(number);
thread01.setName("线程1");
Thread thread02=new Thread(number);
thread02.setName("线程2");
thread01.start();
thread02.start();
}
}
class Number implements Runnable{
private int number=1;
private Object object=new Object();
@Override
public void run() {
while(true){
synchronized (object){
//只有两个线程,该方法会唤醒一个线程。会出现交替输出的现象。
object.notify(); //省略了this.
if (number<=100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
try {
object.wait(); //使得调用线程的阻塞,一旦调用之后会释放锁。
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
4 经典例题:生产者和消费者
简单介绍:生产者与消费者问题
店员一次只能持有固定数量的产品,而消费者从从店员处取走产品,店员一次只能持有固定数量的产品(比如:20)如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位置了就通知消费者来取走产品。
分析:
- 是否存在多线程问题?是,生产者线程,消费者线程。
- 是否有共享数据?是,店员
- 如何解决线程的安全问题?同步机制,有三种方法
- 是否涉及线程的通信?是
代码演示:
public class ProductTest {
public static void main(String[] args) {
Clerk clerk=new Clerk();
Producer producer = new Producer(clerk);
producer.setName("生产者");
Consumer consumer = new Consumer(clerk);
consumer.setName("消费者1");
producer.start();
consumer.start();
}
}
class Clerk{
private int productCount=0;
/**
* 生产产品
*/
public synchronized void produceProduct() {
if (productCount<20){
notify(); //唤醒消费者
productCount++;
System.out.println(Thread.currentThread().getName()+"开始生产第"+productCount+"个产品");
}else {
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 消费产品
*/
public synchronized void consumeProduct() {
if (productCount > 0) {
notify();//唤醒生产者
System.out.println(Thread.currentThread().getName()+"开始消费第"+productCount+"个产品");
productCount--;
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{
private Clerk clerk;
public Producer(Clerk clerk){
this.clerk=clerk;
}
@Override
public
void run() {
System.out.println(Thread.currentThread().getName()+"开始生产产品");
while (true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk){
this.clerk=clerk;
}
@Override
public void run() {
System.out.println(getName()+"开始消费产品");
while(true){
try {
Thread.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
2.9 JDK5 新增的线程创建方式–Callable
1 实现步骤
-
创建一个Callable接口的实现类。
-
实现call(),将操作放到其中。
-
创建Callable接口实现类的对象。
-
将此实现类对象传入到FutureTask的构造器中创建其对象。
-
将FutureTask对象传入到Thread中,并start()启动线程。
可以使用FutureTask对象调用其具体的方法。
get():获取call方法的返回值。
2 理解
1)如何理解实现Callable接口的方式创建多线程比实现Runnable方法更强大?
- call存在返回值。
- call可以抛出异常,被外面的操作捕获,获取异常的信息。
- Callable支持泛型
public class CallableTest {
public static void main(String[] args) {
NumThread numThread = new NumThread();
FutureTask futureTask = new FutureTask<>(numThread);
Thread thread = new Thread(futureTask);
thread.start();
try {
//get()的返回值即为FutureTask构造器参数Callable实现类中call()的返回值。
Object o = futureTask.get();
System.out.println(o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class NumThread implements Callable{
@Override
public Object call() throws Exception {
int sum=0;
for (int i = 0; i <= 100; i++) {
if (i%2==0){
sum+=i;
}
}
return sum;
}
}
2.10 线程池
1 线程池概述
- JDK5新增的方式
- 背景:经常创建和销毁使用量特别大的资源,比如并发情况下的线程,对性能的影响比较大。
- 思路:提前创建好多个线程,放到线程池中,使用时直接获取,使用完放回池中,避免了频繁的创建销毁线程。
2 使用线程池的优点
- 提高了响应速度(减少了创建新线程的时间)
- 降低了资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于对线程管理:corePoolSize(核心池大小),maxNumPoolSize(最大连接数),keepAliveTiem:线程没任务最大保持时间。
3 线程池使用步骤
- 提供指定线程数量的线程池
- 执行指定的线程的操作,需要提供实现Runnable接口或者Callable接口的实现类对象。
- 对于实现了Runable接口的实现类,使用execute来启动线程任务。
- 实现了Callable接口的实现类,使用submit的方式来启动线程任务。
- 关闭连接池。
面试题:创建多线程的方式
public class ThreadPoolTest {
public static void main(String[] args) {
//ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10);
ExecutorService executorService = Executors.newFixedThreadPool(10);
//ExecutorService:是一个接口,里面属性很少,进行线程的管理需要强转为ThreadPoolExecutor来进行
ThreadPoolExecutor service=(ThreadPoolExecutor)executorService;
//适合用于Runnable
//executorService.execute(new NumberThread01());
//executorService.execute(new NumberThread02());
//适合使用Callable
Future submit = executorService.submit(new NumThread01());
Future submit1 = executorService.submit(new NumThread02());
try {
//2550
Object o = submit.get();
//1300
Object o1 = submit1.get();
System.out.println(o);
System.out.println(o1);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//关闭线程池
executorService.shutdown();
}
}
class NumberThread01 implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
class NumberThread02 implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i%2!=0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
class NumThread01 implements Callable{
@Override
public Object call() throws Exception {
int num=0;
for (int i = 0; i <= 100; i++) {
if (i%2==0){
num+=i;
}
}
return num;
}
}
class NumThread02 implements Callable{
@Override
public Object call() throws Exception {
int num=0;
for (int i = 0; i <= 100; i++) {
if (i%4==0){
num+=i;
}
}
return num;
}
}
4 扩展
1 Threadlocal的理解?
首先我们要谈一谈他的作用:
- 为每个线程创建一个副本。
- 为线程的上下文传递对象,多个线程之间。
public class ThreadLocalTest {
private static ThreadLocal<Long> local=new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Task task = new Task();
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
Thread.sleep(10);
thread2.start();
}
static class Task implements Runnable{
@Override
public void run() {
Long result = local.get();
if (result==null){
local.set(System.currentTimeMillis());
System.out.println(Thread.currentThread().getName()+"-->"+local.get());
}
}
}
}
执行结果:
Thread-0-->1603449443184
Thread-1-->1603449443208
为什么结果会不同?
1)看一下get的源码
public T get() {
//1.获取当前线程
Thread t = Thread.currentThread();
//2.当前线程对应的map
ThreadLocalMap map = getMap(t);
//3.如果不为空
if (map != null) {
//3.1 获取当前threadLocal为key,获取到对应的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//4.获取对应entry的value
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
2)set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果当前线程对应的mao:不为空,则赋值;为空,则添加一个map
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
结论:每个线程都有一个自己的map用来保存键值对,如果不是同一个Thread,那么他们的map也是不同的,因此可以实现每一个线程都有一个自己的副本,多线程对同一个threadlocal进行操作的时候,他们的值都是互不干扰的。
2 ThreadLocal的使用场景
1)hibernate管理session,mybatis管理sqlsession,管理connection连接。
无论我们使用什么框架对数据库进行操作,最终都是使用JDBC,都是使用connetion来进行事务的控制,但是连接之类的都是在dao层实现的,我们的事务处理是在service层,如果连接不同,我们的事务肯定是无法控制的,这种情况之下,我们需要保证service中多个dao层操作的的connnetion是同一个,这个时候可以使用ThreadLocal来实现。
实现连接的控制:
alMap map = getMap(t);
//3.如果不为空
if (map != null) {
//3.1 获取当前threadLocal为key,获取到对应的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//4.获取对应entry的value
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
2)set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果当前线程对应的mao:不为空,则赋值;为空,则添加一个map
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
结论:每个线程都有一个自己的map用来保存键值对,如果不是同一个Thread,那么他们的map也是不同的,因此可以实现每一个线程都有一个自己的副本,多线程对同一个threadlocal进行操作的时候,他们的值都是互不干扰的。
2 ThreadLocal的使用场景
1)hibernate管理session,mybatis管理sqlsession,管理connection连接。
无论我们使用什么框架对数据库进行操作,最终都是使用JDBC,都是使用connetion来进行事务的控制,但是连接之类的都是在dao层实现的,我们的事务处理是在service层,如果连接不同,我们的事务肯定是无法控制的,这种情况之下,我们需要保证service中多个dao层操作的的connnetion是同一个,这个时候可以使用ThreadLocal来实现。
实现连接的控制: