JAVA多线程Thread

简介: 多线程Thread

多线程的基本概念

程序、进程、线程

程序(program) 是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码,静态对象

进程(process) 是程序的一次执行过程,或是正在运行的一个程序。是一个动态 的过程:有它自身的产生、存在和消亡的过程。——生命周期

线程(thread) ,进程可进一步细化为线程,是一个程序内部的一条执行路径。

  • 若一个进程同一时间并行执行多个线程,就是支持多线程的
  • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以 访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患

并行与并发

并行 :多个CPU同时执行多个任务。

并发: 一个CPU(采用时间片)同时执行多个任务。

多线程的创建和启动

Thread类

Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread 类来体现。

  • 线程是程序中执行的线程。 Java虚拟机允许应用程序同时执行多个执行线程。
  • 每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。 每个线程可能也可能不会被标记为守护程序。 当在某个线程中运行的代码创建一个新的Thread对象时,新线程的优先级最初设置为等于创建线程的优先级,并且当且仅当创建线程是守护进程时才是守护线程。

Thread类的特性

  • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
  • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

Thread类常用构造方法

Thread() 分配一个新的 Thread对象。
Thread(String name) 创建线程并指定线程实例名
Thread(Runnable target) 指定创建线程的目标对象,它实现了Runnable接 口中的run方法
Thread(Runnable target, String name) 创建新的Thread对象

Thread类常用方法

static Thread currentThread() 返回对当前正在执行的线程对象的引用。
long getId() 返回此线程的标识符。
String getName() 返回此线程的名称。

void join() 等待这个线程死亡。
void run() 如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回。
void setName(String name) 将此线程的名称更改为等于参数 name
void setPriority(int newPriority) 更改此线程的优先级。
static void sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法。
String toString() 返回此线程的字符串表示,包括线程的名称,优先级和线程组。
static void yield() 对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。

int getPriority() 返回此线程的优先级。
Thread.State getState() 返回此线程的状态。
boolean isAlive() 测试这个线程是否活着。

多线程的4种创建方式

方式一:继承Thread类

步骤:

  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run()方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start()方法:启动线程,调用run()方法。

注:

  1. 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
  2. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU 调度决定。
  3. 想要启动多线程,必须调用start()方法。
  4. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上 的异常“IllegalThreadStateException”。

示例

创建并启动一个线程,输出0-99

publicclassMyThreadTest {
publicstaticvoidmain(String[] args) {
//1.创建线程MyThreadmyThread=newMyThread();
//2.启动线程,并调用当前线程的run()方法myThread.start();
    }
}
classMyThreadextendsThread{
publicMyThread() {
    }
//3.重写run()方法@Overridepublicvoidrun() {
for(inti=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

方式二:实现Runnable接口

void run() 当实现接口的对象 Runnable被用来创建一个线程,启动线程使对象的 run在独立执行的线程中调用的方法。

步骤:

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run()方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  1. 调用Thread类的start()方法:开启线程,调用Runnable子类接口的run()方法。

注:

实现Runnable接口,需要通过Thread类的对象调用start()

示例

创建两个分线程,让其中一个线程输出1-100之间的偶数,另一 个线程输出1-100之间的奇数

publicclassMyThreadTest {
publicstaticvoidmain(String[] args) {
//1.创建线程MyThreadonemyThreadone=newMyThreadone();
MyThreadTwomyThreadTwo=newMyThreadTwo();
//2.启动线程,并调用当前线程的run()方法myThreadone.start();
//4.通过Thread类的对象调用start()newThread(myThreadTwo).start();
    }
}
classMyThreadoneextendsThread{
publicMyThreadone() {
    }
//3.重写run()方法@Overridepublicvoidrun() {
//输出1-100之间的偶数for(inti=2;i<=100;i+=2){
System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
classMyThreadTwoimplementsRunnable{
//3.重写run()方法@Overridepublicvoidrun() {
//输出1-100之间的奇数for(inti=1;i<100;i+=2){
System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

方式三:实现Callable接口

Java1.5版本开始,就提供了 CallableFuture 来创建线程

ThreadRunnable两种方式创建线程,不过这两种方式创建线程都有一个缺陷:在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果。

而如果使用CallableFuture,通过它们就可以在任务执行完毕之后得到任务执行结果

Callable产生结果Future获取结果

步骤:

  1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值;
  2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值;
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程;
  4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

注:

在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!

通过FutureTask拿到的,使用.get()方法即可获得线程的返回值,

Callable类

V call()

FutureTask类

构造方法:

FutureTask(Callable<v> allable) 创建一个 FutureTask ,它将在运行时执行给定的 Callable
FutureTask([Runnable runnable, V result) 创建一个 FutureTask ,将在运行时执行给定的 Runnable ,并安排 get将在成功完成后返回给定的结果。

常用方法:

V get() 等待计算完成,然后检索其结果。

示例

runThread(int num)函数中执行线程,创建Callable线程,Callable线程需要执行求第num项斐波那契数列的值,最后在runThread函数中获取Callable线程执行的结果,并打印输出。

publicclassMyThreadTest {
publicstaticvoidmain(String[] args) throwsExecutionException, InterruptedException {
Scannerscanner=newScanner(System.in);
intnum=scanner.nextInt();
//1.创建 Callable 实现类的实例ThreadCallablethreadCallable=newThreadCallable(num);
//2.使用 FutureTask 类来包装 Callable 对象FutureTaskfutureTask=newFutureTask(threadCallable);
//4.使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程newThread(futureTask).start();
//5.调用 FutureTask 对象的 get()方法来获得子线程执行结束后的返回值System.out.println(futureTask.get());
    }
}
classThreadCallableimplementsCallable {
intnum;
//带参构造方法publicThreadCallable(intnum) {
super();
this.num=num;
    }
//3.重写call()方法@OverridepublicIntegercall() throwsException {
returnfun(num);
    }
//输出斐波那契数列publicintfun(intnum) {
if (num<3) {
return1;
        } elsereturnfun(num-1) +fun(num-2);
    }
}

方式四:使用线程池

提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。可以避免频繁创建销毁、实现重复利用。

好处:

  1. 提高响应速度(减少了创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管理

JDK 5.0起提供了线程池相关API:ExecutorServiceExecutors

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

ExecutorService线程池接口类

void shutdown() 启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。
<T> Future<T> submit(Callable<T> task) 提交值返回任务以执行,并返回代表任务待处理结果的Future。

Future<?> submit(Runnable task) 提交一个可运行的任务执行,并返回一个表示该任务的未来。

Executors工具类

static Callable<Object> callable(Runnable task) 执行任务/命令,没有返回值,一般用来执行 Runnable
static ExecutorService newCachedThreadPool() 创建一个根据需要创建新线程的线程池
static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池
ExecutorService newSingleThreadExecutor() 创建一个只有一个线程的线程池

static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行

示例

用线程池创建线程,一个输出奇数,一个输出偶数

publicclassMyTest {
publicstaticvoidmain(String[] args) {
//1.提供指定线程数量的线程池ExecutorServicees=Executors.newFixedThreadPool(10);
ThreadPoolExecutorthreadpool= (ThreadPoolExecutor) es;
//2.执行指定线程池的操作,需要提供实现Runnable接口和Callable接口实现类的对象//提供实现Runnable接口实现类的对象threadpool.execute(newRunnableThread());
//提供实现Callable接口实现类的对象threadpool.submit(newCallableThread());
//3.关闭线程池threadpool.shutdown();
    }
}
classRunnableThreadimplementsRunnable{
@Overridepublicvoidrun() {
for(inti=1;i<=100;i++) {
if(i%2==0)
System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
classCallableThreadimplementsCallable{
@OverridepublicObjectcall() throwsException {
for(inti=1;i<=100;i++) {
if(i%2==1)
System.out.println(Thread.currentThread().getName()+":"+i);
        }
returnnull;
    }
}

线程的状态与调度

线程的状态与调度

如果看懂下图,你对线程的了解就会更上一层楼。

911a4ebdd7e01b3bbee5db2ebd96ab5f.png

  1. 新建状态New):当我们使用new关键字新建一个线程
  2. 就绪状态Runnable);调用start方法启动线程,这个时候就进入了可运行状态
  3. 运行状态Running):就绪状态获取了CPU资源,开始执行run方法;
  4. 阻塞状态Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种;
  • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁);
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中;
  • 其他阻塞:运行的线程执行sleep()join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁);
  1. 死亡状态Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程执行的优先级

Java中线程有优先级,优先级高的线程会获得较多的运行机会。

Java线程的优先级用整数表示,取值范围是1~10Thread类有以下三个静态常量:

staticintMAX_PRIORITY线程可以具有的最高优先级,取值为10。staticintMIN_PRIORITY线程可以具有的最低优先级,取值为1。staticintNORM_PRIORITY分配给线程的默认优先级,取值为5。

如果要设置和获取线程的优先级,可以使用Thread类的setPriority()getPriority()方法。

每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY

线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。

JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

关于线程调度与优先级你还需要了解:

  1. 线程睡眠Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
  2. 线程等待Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用wait(0) 一样。
  3. 线程让步Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
  4. 线程加入join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
  5. 线程唤醒Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

线程常用函数

sleep()函数

sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。

使用方式很简单在线程的内部使用Thread.sleep(millis)即可。

sleep()使当前线程进入停滞状态(阻塞当前线程),让出CPU的使用,目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;

sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。

join()函数

join()函数的定义是指:等待线程终止

yield() 函数

yield函数可以理解为“让步”,它的作用是:暂停当前正在执行的线程对象,并执行其他线程。

yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

wait() 函数和notify()函数

从功能上来说:

  • wait()就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行;
  • 相应的notify()就是对对象锁的唤醒操作。

但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。

Thread.sleep()Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

Volatile关键字

  • 当一个共享变量被volatile修饰时,它就具备了“可见性”,即这个变量被一个线程修改时,这个改变会立即被其他线程知道。
  • 当一个共享变量被volatile修饰时,会禁止“指令重排序”。

线程同步

并发编程什么时候会出现安全问题

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有 执行完,另一个线程参与进来执行。导致共享数据的错误。这个就是线程安全问题,多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果

synchronized关键字

互斥锁,互斥锁:就是能达到互斥访问目的的锁。

如果对一个变量加上互斥锁,那么在同一时刻,该变量只能有一个线程能访问,即当一个线程访问临界资源时,其他线程只能等待。

在Java中,每一个对象都有一个锁标记(monitor),也被称为监视器,当多个线程访问对象时,只有获取了对象的锁才能访问。

使用synchronized修饰对象的方法或者代码块,当某个线程访问这个对象synchronized方法或者代码块时,就获取到了这个对象的锁,

这个时候其他对象是不能访问的,只能等待获取到锁的这个线程执行完该方法或者代码块之后,才能执行该对象的方法。

在使用synchronized关键字的时候有几个问题需要我们注意:

在线程调用synchronized的方法时,其他synchronized的方法是不能被访问的,道理很简单,一个对象只有一把锁;
当一个线程在访问对象的synchronized方法时,其他线程可以访问该对象的非synchronized方法,因为访问非synchronized不需要获取锁,是可以随意访问的;
如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

synchronized代码块

synchronized(synObject) {
//共享代码块}

当在某个线程中执行该段代码时,该线程会获取到该对象的synObject锁,此时其他线程无法访问这段代码块,synchronized的值可以是this代表当前对象,也可以是对象的属性,用对象的属性时,表示的是对象属性的锁。

synchronized的锁是什么?

  • 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
  • 同步方法的锁:静态方法(类名.class)、非静态方法(this)
  • 同步代码块:自己指定,很多时候也是指定为this或类名.class

注意:

  • 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就 无法保证共享资源的安全
  • 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方 法共用同一把锁(this),同步代码块(指定需谨慎)

Lock(锁)

classA {
privatefinalReentrantLocklock=newReenTrantLock();
publicvoidm() {
lock.lock();
try {
//保证线程安全的代码;        } finally {
lock.unlock();
        }
    }
}

通过显式定义同 步锁对象来实现同步。同步锁使用Lock对象充当。

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。

ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以 显式加锁、释放锁。

synchronized 与 Lock 的对比

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是 隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有 更好的扩展性(提供更多的子类)

优先使用顺序:

Lock → 同步代码块(已经进入了方法体,分配了相应资源) → 同步方法 (在方法体之外)


目录
相关文章
|
1天前
|
Java
Java一分钟:线程协作:wait(), notify(), notifyAll()
【5月更文挑战第11天】本文介绍了Java多线程编程中的`wait()`, `notify()`, `notifyAll()`方法,它们用于线程间通信和同步。这些方法在`synchronized`代码块中使用,控制线程执行和资源访问。文章讨论了常见问题,如死锁、未捕获异常、同步使用错误及通知错误,并提供了生产者-消费者模型的示例代码,强调理解并正确使用这些方法对实现线程协作的重要性。
9 3
|
1天前
|
安全 算法 Java
Java一分钟:线程同步:synchronized关键字
【5月更文挑战第11天】Java中的`synchronized`关键字用于线程同步,防止竞态条件,确保数据一致性。本文介绍了其工作原理、常见问题及避免策略。同步方法和同步代码块是两种使用形式,需注意避免死锁、过度使用导致的性能影响以及理解锁的可重入性和升级降级机制。示例展示了同步方法和代码块的运用,以及如何避免死锁。正确使用`synchronized`是编写多线程安全代码的核心。
22 2
|
1天前
|
安全 Java 调度
Java一分钟:多线程编程初步:Thread类与Runnable接口
【5月更文挑战第11天】本文介绍了Java中创建线程的两种方式:继承Thread类和实现Runnable接口,并讨论了多线程编程中的常见问题,如资源浪费、线程安全、死锁和优先级问题,提出了解决策略。示例展示了线程通信的生产者-消费者模型,强调理解和掌握线程操作对编写高效并发程序的重要性。
21 3
|
1天前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第11天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个方面,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。我们将通过实例和代码片段来说明这些概念和技术。
2 0
|
1天前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。
|
2天前
|
安全 Java
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
|
2天前
|
安全 Java
【JAVA进阶篇教学】第六篇:Java线程中状态
【JAVA进阶篇教学】第六篇:Java线程中状态
|
2天前
|
缓存 Java
【JAVA进阶篇教学】第五篇:Java多线程编程
【JAVA进阶篇教学】第五篇:Java多线程编程
|
2天前
|
Java
【JAVA基础篇教学】第十二篇:Java中多线程编程
【JAVA基础篇教学】第十二篇:Java中多线程编程
|
3天前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
13 1