java多线程
1.多线程的基本概念
线程指进程中的一个执行场景,也就是执行流程,可以理解为:
我在电脑上开启的一个植物大战僵尸的游戏,这个游戏就相当于一个进程,而在这个游戏里面的每一个小场景就相当于线程。进程如果是一个大公司在生产运作,线程也就是每一个员工正在工作
- 每个进程是一个应用程序,都有独立的内存空间
- 同一个进程中的线程共享其进程中的内存和资源
2.在java中线程的创建和启动
Java 虚拟机的主线程入口是 main 方法,用户可以自己创建线程,创建方式有两种
- 继承 Thread类
- 实现 Runnable 接口
但是由于java舍弃了C++的多继承性,所以推荐使用 Runnable 接口
能实现一定实现,尽量使用接口
2.1继承Thread类
Thread 类中创建线程最重要的两个方法为:
public void run()//运行方法
public void start()//启动方法,会自动调用运行方法
采用 Thread类创建线程,只需要继承 Thread,覆盖 Thread 中的 run 方法,父类 Thread中的 run 方法没有抛出异常,那么子类也不能抛出异常,最后采用 start 启动线程即可
MyThread继承Thread
public class test01{
public static void main(String[] args) {
//创建
Thread thread=new MyThread();
//启动
thread.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
}
如上代码,在主函数中并没有调用run方法,控制台依旧会输出
0
1
2
3
4
Process finished with exit code 0
如下代码只有一个主线程
public class test01{
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.run();
test();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread{
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
}
可以发现,这个程序只有一个主线程,所以说最先调用的run的方法如果不执行完成,test()就永远不可能被调用,这段程序就是只有一个线程
运行结果
0
1
2
3
4
我是test方法
Process finished with exit code 0
将这段代码修改
public class test01{
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
test();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread extends Thread{
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
}
这里MyThread
继承了Thread
运行结果如下
我是test方法
0
1
2
3
4
Process finished with exit code 0
这里明明是在myThread.start();后面的test()却先被执行了,现在就是两个线程,两个线程同时执行互不干预
012345可以输出输出出来是因为创建了一个新的线程在执行
MyThread myThread = new MyThread();
myThread.start();
test()这个这个方法不在新的线程之内,在主之内。通过输出结果大家会看到,没有顺序执行,而在输出数字的同时执行了 test方法,如果从效率上看,采用多线程的示例要快些,因为我们可以看作他是同时执行的,test方法没有等待前面的操作完成才执行,这叫“异步编程模型”
在这段代码中,主线程和MyThread
线程是并发执行的,它们的执行顺序是不确定的,取决于线程调度器的调度策略。
在主线程中,首先创建并启动了MyThread
线程,然后立即调用了test()
方法打印输出。因为MyThread
线程和主线程是并发执行的,它们的执行顺序是不确定的。有可能test()
方法会在MyThread
线程启动前就被调用,也有可能在MyThread
线程启动后才被调用。
因为MyThread
线程在启动后需要一些时间才能真正开始执行run()
方法,这就给了主线程足够的时间来调用test()
方法并输出了"我是test方法"。但这种情况是不可靠的,因为它取决于多个因素,如线程调度器的调度策略、处理器的性能、线程的优先级等等。
在一些特殊情况下,可能会出现MyThread
线程的优先级比主函数的优先级大的情况。例如,如果在创建MyThread
线程之前,主函数调用了Thread.currentThread().setPriority()
方法来设置当前线程的优先级,而这个优先级比5更低,那么MyThread
线程的优先级就会比主函数的优先级高。
另外,一些操作系统可能会对不同线程的优先级进行调整,从而影响线程的执行顺序。例如,在某些操作系统中,当一个线程持续占用CPU时间过长时,系统可能会降低它的优先级,同时提高其他线程的优先级,以实现公平调度。
不过,线程的优先级只是一种参考值,实际上并不能保证线程的执行顺序。因此,在编写多线程程序时,应该避免依赖于线程的优先级,而要采用同步、锁等机制来保证程序的正确性。
2.2实现Runnable接口
public class test01{
public static void main(String[] args) {
Runnable myThread = new MyThread();
myThread.start();//飘红
test();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread implements Runnable {
@Override
public void run() {
}
}
myThread.start();会飘红,原因是implements 的Runnable并没有start(),在idea中按住ALT+左键单击查看Runnable会发现:
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface {@code Runnable} is used
* to create a thread, starting the thread causes the object's
* {@code run} method to be called in that separately executing
* thread.
* <p>
* The general contract of the method {@code run} is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
只有一个run()
可以通过以下方式来使用start()
public class test01{
public static void main(String[] args) {
Runnable myThread = new MyThread();
new Thread(myThread).start();
test();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
}
当然,可以用lambda函数来优化以上的代码
public class test01{
public static void main(String[] args) {
Runnable runnable=()->{
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
};
test();
}
public static void test(){
System.out.println("我是test方法");
}
}
如上方法使用了Java 8引入的Lambda表达式来创建Runnable
对象。Lambda表达式相对于匿名内部类和传统的函数式接口实现,具有以下几个优点:
- 简洁性:Lambda表达式可以使代码更加简洁,避免了大量的样板代码,使得代码更加易于阅读和维护。
- 可读性:Lambda表达式更加直观和易读,能够更好地表达程序员的意图,使得程序更加易于理解。
- 灵活性:Lambda表达式可以在需要时轻松地创建函数式接口的实例,而不需要显式地实现一个接口或创建匿名内部类的实例。
- 高效性:Lambda表达式在运行时会被转换为函数式接口的实例,它的执行效率与传统的函数式接口实现相当,甚至更高。
因此,使用Lambda表达式可以使代码更加简洁、易读、灵活和高效。不过,在使用Lambda表达式时也需要注意一些限制和注意事项,比如Lambda表达式只能用于函数式接口,不能访问非final的局部变量等。
3.线程的生命周期
线程的生命周期存在五个状态:新建、就绪、运行、阻塞、死亡
新建: 采用 new 语句创建完成
就绪:执行 start 后
运行: 占用 CPU 时间
阻塞:执行了 wait 语句、执行了 sleep 语句和等待某个对象锁,IO 处理
终止:退出 run()方法
4.优先级
线程的优先级可以手动设置
public class test01{
public static void main(String[] args) {
Runnable myThread = new MyThread();
Thread thread01=new Thread(myThread,"第一个线程");
thread01.setPriority(Thread.MAX_PRIORITY);
thread01.start();
Thread thread02=new Thread(myThread,"第二个线程");
thread02.setPriority(Thread.MIN_PRIORITY);
thread02.start();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 101; i++) {
System.out.println(i);
}
}
}
运行结果
第1个线程:0
第1个线程:1
第2个线程:0
第1个线程:2
第2个线程:1
第1个线程:3
第2个线程:2
第1个线程:4
第2个线程:3
第1个线程:5
......
从前几个结果就可以看出并不是执行0-100后输出第二组0-100,
从以上输出结果应该看可以看出,优先级高的线程会得到的 CPU 时间多一些,优先执行完成
5.线程调度和控制
5.1方法Thread.sleep
public class test01{
public static void main(String[] args) {
Runnable myThread = new MyThread();
Thread thread01=new Thread(myThread,"第1个线程");
thread01.setPriority(Thread.MAX_PRIORITY);
thread01.start();
Thread thread02=new Thread(myThread,"第2个线程");
thread02.setPriority(Thread.MIN_PRIORITY);
thread02.start();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 101; i++) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
这里设置睡眠1毫秒
运行结果
第1个线程:0
第2个线程:0
第1个线程:1
第2个线程:1
第2个线程:2
第1个线程:2
第2个线程:3
第1个线程:3
第1个线程:4
第2个线程:4
第2个线程:5
第1个线程:5
第1个线程:6
第2个线程:6
第2个线程:7
.......
sleep 设置休眠的时间,单位毫秒,当一个线程遇到 sleep 的时候,就会睡眠,进入到阻塞状态,放弃 CPU,腾出 CPU 时间片,给其他线程用,所以在开发中通常我们会这样做,使其他的线程能够取得 CPU 时间片,当睡眠时间到达了,线程会进入就绪状态,得到 CPU 时间片继续执行,如果线程在睡眠状态被中断了,将会抛出 IterruptedException
Thread.sleep()
方法是Java中的一个静态方法,它可以使当前线程暂停执行一段时间,以便其他线程有机会执行。Thread.sleep()
方法的特点如下:
-
Thread.sleep()
方法是一个静态方法,它可以在任何地方使用,而不需要创建线程对象。 -
Thread.sleep()
方法会让当前线程暂停执行指定的时间,以毫秒为单位。在这段时间内,当前线程不会获得任何CPU时间片,其他线程有机会获得CPU时间片并执行。 -
Thread.sleep()
方法可能会抛出InterruptedException
异常,这是因为当线程在睡眠期间被中断时,就会抛出这个异常。因此,当调用Thread.sleep()
方法时,应该捕获并处理这个异常。 -
Thread.sleep()
方法不能保证线程会在指定的时间后立即恢复执行,而是只能保证线程在指定时间后处于可运行状态(就是准备就绪),具体的执行时间还要取决于系统的调度器。 -
Thread.sleep()
不释放线程锁。(Thread.sleep()
方法不会释放当前线程持有的锁,这意味着其他线程在等待此锁时仍然会被阻塞。具体来说,当一个线程调用Thread.sleep()
方法时,它会暂停执行指定的时间,但是它仍然持有它所持有的锁,直到睡眠时间结束才会释放锁。其他线程如果需要获取该锁,则必须等待当前线程执行完毕并释放锁后才能继续执行。同时也导致一些问题,比如死锁或者线程饥饿。如果线程持有一个重要的锁,并且在执行期间调用了
Thread.sleep()
方法,那么其他需要该锁的线程可能会长时间地阻塞或者无法获得锁,从而导致死锁或者线程饥饿的问题。)
总的来说,Thread.sleep()
方法可以使当前线程暂停执行一段时间,以便其他线程有机会执行。不过,它的使用也需要注意一些限制和注意事项,比如捕获InterruptedException
异常、避免使用过长的睡眠时间等。
5.2方法Thread.yield
public class test01{
public static void main(String[] args) {
Runnable myThread = new MyThread();
Thread thread01=new Thread(myThread,"第1个线程");
thread01.setPriority(Thread.MAX_PRIORITY);
thread01.start();
Thread thread02=new Thread(myThread,"第2个线程");
thread02.setPriority(Thread.MIN_PRIORITY);
thread02.start();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 101; i++) {
Thread.yield();
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
执行结果
第1个线程:0
第2个线程:0
第1个线程:1
第1个线程:2
第1个线程:3
第2个线程:1
第1个线程:4
第1个线程:5
第2个线程:2
第1个线程:6
第1个线程:7
第2个线程:3
第1个线程:8
第1个线程:9
第2个线程:4
第1个线程:10
第2个线程:5
第1个线程:11
第2个线程:6
第1个线程:12
第1个线程:13
.......
Thread.yield()
和Thread.sleep()
都是Java中的线程控制方法,它们有以下的区别:
- 功能不同:
Thread.yield()
是一个静态方法,它会让当前线程让出CPU资源,给其他线程更多的机会来运行。Thread.sleep()
也是一个静态方法,它会让当前线程休眠一段时间,以便其他线程有机会运行。 - 时间参数不同:
Thread.yield()
方法不需要参数,它只是让当前线程让出CPU,而Thread.sleep()
方法需要一个时间参数,指定当前线程休眠的时间长度,以毫秒为单位。 - 抛出异常不同:
Thread.yield()
方法不会抛出异常,而Thread.sleep()
方法可能会抛出InterruptedException
异常,因为线程在睡眠期间可能会被中断。 - 效果不同:
Thread.yield()
方法的作用是让当前线程让出CPU,但是它并不能保证其他线程会立即获得CPU资源,因为这取决于操作系统的调度器。而Thread.sleep()
方法则可以确保当前线程在指定的时间后会重新运行,因为它会让当前线程休眠指定的时间长度。
总体来说,Thread.yield()
和Thread.sleep()
方法都可以用来控制线程的执行顺序和并发度,但是它们的作用和效果不同。通常来说,Thread.yield()
方法主要用于在调试和测试程序时强制让出CPU资源,以便在多线程环境中更好地观察线程的执行顺序和并发度,而Thread.sleep()
方法则主要用于在程序中临时休眠当前线程,以便其他线程有机会运行。
yield()
方法可以让当前线程让出CPU资源,让其他线程有机会运行,而其他线程的优先级并不一定要和当前线程相同。具体来说,yield()
方法会让当前线程进入就绪状态,并让操作系统调度执行其他线程。当其他线程执行完毕或被阻塞后,操作系统会再次调度当前线程执行。
5.3方法Thread.join
Thread.join()
是一个实例方法,用于等待当前线程执行完毕后再继续执行下面的代码。当一个线程调用另一个线程的join()
方法时,该线程会被阻塞,直到其他线程执行完毕并退出。
package com.cxy.Thread;
public class test01{
public static void main(String[] args) throws InterruptedException {
Runnable myThread = new MyThread();
Thread thread01=new Thread(myThread,"第1个线程");
thread01.setPriority(Thread.MAX_PRIORITY);
thread01.start();
thread01.join();
Thread thread02=new Thread(myThread,"第2个线程");
thread02.setPriority(Thread.MIN_PRIORITY);
thread02.start();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
执行结果
第1个线程:0
第1个线程:1
第1个线程:2
第1个线程:3
第1个线程:4
第2个线程:0
第2个线程:1
第2个线程:2
第2个线程:3
第2个线程:4
Process finished with exit code 0
5.4 interrupt(中断)
在Java中,interrupt()
是一个用于中断线程的方法。当一个线程正在执行sleep()
、wait()
或join()
等阻塞方法时,如果另一个线程调用了该线程的interrupt()
方法,那么该线程将会被中断,抛出一个InterruptedException
异常。
具体来说,interrupt()
方法有以下几种作用:
- 如果线程处于阻塞状态,那么调用
interrupt()
方法将会使线程抛出InterruptedException
异常,从而打破阻塞状态,让线程可以继续执行。 - 如果线程处于非阻塞状态,那么调用
interrupt()
方法将会设置线程的中断标志位,表示该线程已经被中断。可以通过isInterrupted()
方法来检查线程的中断状态。 - 如果线程正在执行
Thread.sleep()
方法,那么调用interrupt()
方法将会使线程抛出InterruptedException
异常,并清除中断标志位。
public class test01{
public static void main(String[] args) throws InterruptedException {
Runnable myThread = new MyThread();
Thread thread=new Thread(myThread,"线程1");
thread.start();
thread.interrupt();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
执行结果
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at com.cxy.Thread.MyThread.run(test01.java:18)
at java.base/java.lang.Thread.run(Thread.java:833)
线程1:0
线程1:1
线程1:2
线程1:3
线程1:4
Process finished with exit code 0
5.5正确停止线程
在Java中,停止一个线程有多种方式,但是要注意线程停止的安全性和正确性,避免出现死锁、竞态条件等问题。下面介绍几种常见的线程停止方式。
- 使用标志位停止线程
这是一种常见的线程停止方式,即在线程内部维护一个标志位,当该标志位为 true 时,线程停止执行。示例代码如下:
public class MyThread extends Thread {
private volatile boolean stopped = false;
@Override
public void run() {
while (!stopped) {
// 线程执行的代码
}
}
public void stopThread() {
stopped = true;
}
}
在上面的示例代码中,我们定义了一个stopped
标志位,当该标志位为 true 时,线程停止执行。在run()
方法中,我们使用 while 循环检查该标志位的值,如果为 false,则继续执行线程,否则退出循环。在stopThread()
方法中,我们将stopped
标志位设置为 true,从而停止线程的执行。
需要注意的是,使用标志位停止线程需要保证线程执行的代码是可中断的,即可以在任意时刻中断执行,并且中断后不会产生不良后果。此外,需要使用volatile
关键字修饰标志位,以保证多线程环境下的可见性。
- 使用 interrupt() 方法停止线程
interrupt()
方法是用于中断线程的方法,当一个线程处于阻塞状态时,可以通过调用该方法使其抛出InterruptedException
异常,从而打破阻塞状态,继续执行线程。示例代码如下:
public class MyThread extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// 线程执行的代码
}
}
public void stopThread() {
interrupt();
}
}
在上面的示例代码中,我们在run()
方法中使用isInterrupted()
方法检查线程的中断状态,如果为 true,则退出循环。在stopThread()
方法中,我们调用interrupt()
方法中断线程的执行。
需要注意的是,使用interrupt()
方法停止线程需要保证线程执行的代码是可中断的,并且在捕获InterruptedException
异常后,需要重新设置线程的中断标志位,以便在下一次调用isInterrupted()
方法时返回正确的结果。
- 使用 stop() 方法停止线程
stop()
方法是用于停止线程的方法,可以立即终止线程的执行。但是,由于该方法会导致线程立即停止,不会释放线程占用的资源,因此不推荐使用。示例代码如下:
public class MyThread extends Thread {
@Override
public void run() {
while (true) {
// 线程执行的代码
}
}
public void stopThread() {
stop();
}
}
在上面的示例代码中,我们在run()
方法中使用一个无限循环来模拟线程的执行,然后在stopThread()
方法中调用stop()
方法停止线程的执行。
需要注意的是,使用stop()
方法会导致线程立即停止,不会释放线程占用的资源,容易导致死锁、不一致等问题,因此不推荐使用。
5.6 wait()
在Java中,wait()
方法是Object
类中的一个方法,它用于使当前线程进入等待状态,直到其他线程调用notify()
或notifyAll()
(唤醒所有)方法唤醒它。wait()
方法一般会与synchronized
关键字一起使用,以确保线程的安全性。
wait()
特点
wait()
方法会释放线程持有的锁,以允许其他线程访问该对象。这是为了避免死锁的情况出现。- 当调用
wait()
方法时,线程会进入等待状态,并且不会再次进入运行状态直到被唤醒。当其他线程调用该对象的notify()
或notifyAll()
方法时,等待的线程会重新进入运行状态。 wait()
方法可以使用带超时时间的重载方法,如果在指定时间内没有被唤醒,线程会自动进入运行状态。- 在调用
wait()
方法时,线程必须拥有该对象的锁,否则会抛出IllegalMonitorStateException
异常。 wait()
方法可以和synchronized
关键字一起使用,以实现线程之间的同步和通信。
互相等待,互相唤醒
import java.util.function.Consumer;
public class test01{
public static void main(String[] args){
//创建线程
Demo demo=new Demo();
new Product(demo);
new myConsumer(demo);
}
}
class Demo{
int value;
boolean flag=false;
public synchronized int getValue() throws InterruptedException {
if (flag) {
wait();//等待
flag=false;
}
notify();//唤醒
System.out.println("getValue:");
return value;
}
public synchronized void setValue(int value) throws InterruptedException {
if (!flag){
wait();
flag=true;
}
notify();
System.out.println("setValue:");
this.value=value;
}
}
class Product implements Runnable{
private Demo demo;
public Product(Demo demo){
this.demo=demo;
new Thread(this).start();
}
@Override
public void run() {
int i=0;
while (true){
System.out.println("生产者--->" + i);
try {
demo.setValue(i++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class myConsumer implements Runnable{
private Demo demo;
public myConsumer(Demo demo){
this.demo=demo;
new Thread(this).start();
}
@Override
public void run() {
while (true){
try {
System.out.println("消费者---->"+demo.getValue());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
以上代码使用wait()和notify()方法实现了一个简单的生产者和消费者模型,其中Demo类作为共享资源,Product类实现生产者,myConsumer类实现消费者。
运行结果
.....
setValue:
生产者--->42731
getValue:
消费者---->42730
getValue:
消费者---->42730
getValue:
消费者---->42730
setValue:
生产者--->42732
setValue:
生产者--->42733
......
6.线程同步(上锁)
6.1问题
public class test01{
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread=new Thread(myThread,"线程1");
thread.start();
Thread thread1=new Thread(myThread,"线程2");
thread1.start();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread implements Runnable {
@Override
public void run() {
int sum=0;//局部变量
for (int i = 0; i < 101; i++) {
sum+=i;
}
System.out.println(sum);
}
}
在Java中,变量可以分为局部变量和成员变量。局部变量是定义在方法中的变量,其作用域只在方法内部,方法执行完毕后,局部变量就会被销毁。成员变量是定义在类中、方法外部的变量,其作用域是整个类,这意味着它可以被类中的任何方法访问。成员变量的生命周期与对象的生命周期相同,当对象被销毁时,成员变量也会被销毁。
sum
变量是定义在run()
方法中的局部变量,因此它只在run()
方法内部有效,每个线程都有自己的sum
变量,互不干扰,所以两个线程输出的结果是相同的。
public class test01{
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread=new Thread(myThread,"线程1");
thread.start();
Thread thread1=new Thread(myThread,"线程2");
thread1.start();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread implements Runnable {
private int sum=0;//成员变量
@Override
public void run() {
for (int i = 0; i < 101; i++) {
sum+=i;
}
System.out.println(sum);
}
}
运行结果
10100
10100
Process finished with exit code 0
再次运行
5050
10100
Process finished with exit code 0
在多线程环境下,由于线程之间的执行是并发的,因此可能会出现竞态条件(Race Condition)的情况,导致程序输出的结果不确定。
多个线程都共享同一个sum
变量,而每个线程都会对sum
变量进行写操作,因此可能会出现数据不一致的情况。例如,当一个线程正在对sum
变量进行写操作时,另一个线程也可能同时对sum
变量进行写操作,这样就可能导致数据紊乱。因此这就是线程不安全。
6.2上锁
关键字synchronized上锁,同步块
使用线程同步
public class test01{
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread=new Thread(myThread,"线程1");
thread.start();
Thread thread1=new Thread(myThread,"线程2");
thread1.start();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread implements Runnable {
private int sum=0;
@Override
public synchronized void run() {
for (int i = 0; i < 101; i++) {
sum+=i;
}
System.out.println(sum);
sum=0;// 重置sum初始值
}
}
在上述代码中,使用synchronized
关键字修饰run()
方法,这样就可以保证在同一时刻只有一个线程可以对sum
变量进行写操作,从而避免了数据不一致的问题。但是线程,它们共享同一个MyThread
对象的实例变量sum
。当这两个线程同时执行run()
方法时,会对sum
变量进行累加,但是它们的执行顺序是不确定的。如果线程1先执行完run()
方法,输出了5050,那么此时sum
变量的值为0。此时,线程2开始执行run()
方法时,会对sum
变量进行累加,此时sum
变量的初始值为0,因此线程2最终输出的结果也是5050。但如果线程2先执行完run()
方法,输出了10100,那么此时sum
变量的值为0,线程1开始执行run()
方法时,会对sum
变量进行累加,此时sum
变量的初始值为10100,因此线程1最终输出的结果为10100。
运行结果
5050
10100
Process finished with exit code 0
在方法内上锁
public class test01{
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread=new Thread(myThread,"线程1");
thread.start();
Thread thread1=new Thread(myThread,"线程2");
thread1.start();
}
public static void test(){
System.out.println("我是test方法");
}
}
class MyThread implements Runnable {
private int sum=0;
@Override
public void run() {
synchronized (this){
for (int i = 0; i < 101; i++) {
sum+=i;
}
System.out.println(sum);
sum=0;// 重置sum初始值
}
}
}
这个写法的优点在于使用了对象锁synchronized (this)
来保证同步,而不是使用synchronized
修饰run()
方法。这种方式可以更加精细地控制同步范围,避免不必要的锁等待,提高程序的并发性能。
具体来说,这个写法中synchronized (this)
锁定的是MyThread
对象实例,而不是整个run()
方法。这样,当多个线程并发执行run()
方法时,它们会依次获取MyThread
对象实例的锁来执行同步代码块中的代码,避免了多个线程同时获取锁的情况,提高了程序的并发性能。
另外,这个写法还将实例变量sum
的定义放到了run()
方法内部,避免了多个线程对同一个实例变量进行累加的情况,也提高了程序的并发性能。
简单来说如果我的run()方法里面有1000行代码,但是其中只有一行需要上锁,使用synchronized (this)
的优点就体现出来了
运行结果
5050
5050
Process finished with exit code 0
7.守护线程
Java中的守护线程(Daemon Thread)是一种特殊的线程,它的作用是为其他非守护线程提供服务。当所有的非守护线程都结束时,守护线程也会随之结束。因此,守护线程通常用于在后台执行一些任务,如垃圾回收、日志记录等,以提高系统的性能和稳定性。
Java中的守护线程可以通过设置线程对象的setDaemon(true)
方法来创建,当线程对象被设置为守护线程后,它将自动随着所有的非守护线程的结束而结束。另外,在Java中,主线程也是非守护线程,因此如果在主线程中创建的子线程没有被设置为守护线程,那么即使主线程结束了,子线程仍然会继续执行。
在Java中,守护线程与非守护线程的区别在于,当所有的非守护线程结束时,JVM会自动退出,而不管守护线程是否执行完毕。因此,守护线程通常用于执行一些不太重要的任务,如日志记录、监控等,以提高系统的性能和稳定性。
需要注意的是,守护线程不能用于执行需要释放资源的任务,因为它们会在所有非守护线程结束时强制退出,无法保证资源的正确释放。因此,在编写守护线程时,需要仔细考虑任务的性质和需要处理的资源,确保它们不会对系统的稳定性和安全性造成影响。
非守护线程代码
public class test01{
public static void main(String[] args){
MyThread myThread=new MyThread();
new Thread(myThread).start();
System.out.println("主函数运行结束");
}
}
class MyThread implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
输出
主函数运行结束
0
1
2
3
4
Process finished with exit code 0
主函数结束了,还是继续输出了后续内容
public class test01{
public static void main(String[] args){
MyThread myThread=new MyThread();
Thread thread = new Thread(myThread, "守护线程");
thread.setDaemon(true);
thread.start();
System.out.println("主函数运行结束");
}
}
class MyThread implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
设置了守护现场输出
主函数运行结束
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Process finished with exit code 0
程序中的守护线程只是简单地输出数字0到99,因此在主函数打印输出"主函数运行结束"之后,即使守护线程没有执行完毕,程序也会正常结束。这是因为,在Java中,所有非守护线程结束后,JVM会自动退出,而不管守护线程是否执行完毕,守护线程是为用户线程服务的,当用户线程全部结束,守护线程会自动结束。
8.线程池
Java中的线程池是一种管理和复用线程的机制,它可以避免为每个新的任务创建新线程而带来的开销,提高程序的性能和响应速度。Java中的线程池是通过Executor框架来实现的。
线程池的核心接口是Executor和ExecutorService。Executor是一个简单的接口,它只有一个方法execute(Runnable command),用于执行指定的任务。而ExecutorService是Executor接口的扩展,它提供了更多的方法,如submit()、invokeAll()、invokeAny()等,用于提交任务、批量执行任务和获取任务执行结果等。
Java中的线程池一般由ThreadPoolExecutor类来实现,它实现了ExecutorService接口。ThreadPoolExecutor类的构造方法可以设置线程池的基本属性,如核心线程数、最大线程数、线程存活时间、工作队列等。线程池中的任务可以是Runnable或Callable类型的对象。
使用线程池可以提高程序的性能和响应速度,同时也能避免由于过多的线程导致的系统资源浪费和线程调度的开销。线程池的使用需要注意线程安全和资源管理等问题,如果不慎使用不当,也可能会导致程序的性能和可靠性问题。在使用线程池时,需要根据实际情况调整线程池的配置参数,以达到最优的性能和资源利用率。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test01 {
public static void main(String[] args) {
// 创建线程池,大小为5
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 提交10个任务给线程池
for (int i = 0; i < 10; i++) {
executorService.submit(() -> {
// 输出当前线程的名字,并让线程休眠1秒钟
System.out.println("线程名:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executorService.shutdown();
}
}
运行结果
线程名:pool-1-thread-2
线程名:pool-1-thread-1
线程名:pool-1-thread-3
线程名:pool-1-thread-5
线程名:pool-1-thread-4
线程名:pool-1-thread-2
线程名:pool-1-thread-3
线程名:pool-1-thread-1
线程名:pool-1-thread-4
线程名:pool-1-thread-5
......
因为是固定大小的线程池,出现的永远是1-5线程
Executors
Java中的Executors类是一个工厂类,用于创建各种类型的线程池和任务调度器。它提供了一些静态工厂方法,可以方便地创建线程池和任务调度器,避免了手动编写线程池和任务调度器的繁琐过程。
Executors类提供了以下常用的方法:
// 创建一个单线程的线程池,保证所有任务按照指定顺序在一个线程中执行
public static ExecutorService newSingleThreadExecutor();
// 创建一个固定大小的线程池,线程数为nThreads
public static ExecutorService newFixedThreadPool(int nThreads);
// 创建一个可缓存的线程池,线程数根据需要自动增加,空闲线程会在60秒内被回收
public static ExecutorService newCachedThreadPool();
// 创建一个定时任务的线程池,corePoolSize为核心线程数
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
// 创建一个工作窃取线程池,用于实现流式计算 (fork-join)
public static ExecutorService newWorkStealingPool(int parallelism);
Executors类的方法返回的都是ThreadPoolExecutor或ScheduledThreadPoolExecutor对象,它们是ExecutorService接口的具体实现类。
8.1创建一个单线程的线程池
要创建一个单线程的线程池,可以使用Executors类的newSingleThreadExecutor()
静态工厂方法。该方法返回一个ExecutorService对象,它是单线程池的抽象接口,提供了提交任务和关闭线程池等方法。
以下是创建单线程池的示例代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutorExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int taskId = i;
executorService.submit(() -> {
System.out.println("Task #" + taskId + " is running on thread " + Thread.currentThread().getName());
});
}
executorService.shutdown();
}
}
在这个示例代码中,我们创建了一个单线程池,然后提交了10个任务给线程池。每个任务都是一个Lambda表达式,输出任务编号和当前线程的名字。最后,调用executorService.shutdown()
方法关闭线程池,等待所有任务执行完毕。
8.2 创建固定数量线程池
创建一个固定数量的线程池可以使用Java中的Executor框架。
下面是一个示例代码,它创建一个大小为5的线程池,并使用该线程池执行10个任务:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小为5的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交10个任务给线程池执行
for (int i = 0; i < 10; i++) {
executor.submit(new Task(i));
}
// 关闭线程池
executor.shutdown();
}
}
class Task implements Runnable {
private int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task #" + taskId + " is running.");
}
}
在这个示例代码中,我们使用Executors.newFixedThreadPool(5)
创建一个固定大小为5的线程池。然后,我们提交10个任务给线程池执行,每个任务都是一个Task
对象,实现了Runnable
接口中的run()
方法。最后,我们调用executor.shutdown()
关闭线程池。
8.3 创建可缓存的线程池
newCachedThreadPool()
是Java中的一个线程池工厂方法,它会创建一个可缓存的线程池。这种类型的线程池会根据需要创建新线程,但会在先前创建的线程可用时重用它们。如果线程池中的线程在60秒内没有被使用,则它们将被终止并从线程池中删除。
newCachedThreadPool()
方法返回一个ExecutorService
对象,可以通过该对象向线程池提交任务。下面是一个示例代码,它使用newCachedThreadPool()
创建一个可缓存的线程池,并使用该线程池执行10个任务:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
// 创建一个可缓存的线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 提交10个任务给线程池执行
for (int i = 0; i < 10; i++) {
executor.submit(new Task(i));
}
// 关闭线程池
executor.shutdown();
}
}
class Task implements Runnable {
private int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task #" + taskId + " is running.");
}
}
在这个示例代码中,我们使用Executors.newCachedThreadPool()
创建一个可缓存的线程池。然后,我们提交10个任务给线程池执行,每个任务都是一个Task
对象,实现了Runnable
接口中的run()
方法。最后,我们调用executor.shutdown()
关闭线程池。
需要注意的是,由于可缓存的线程池可以创建无限数量的线程,因此需要格外小心,以免在高负载情况下导致系统资源耗尽。
8.4 创建定时任务的线程池
创建定时任务的线程池可以使用Java中的ScheduledExecutorService
接口。ScheduledExecutorService
接口扩展了ExecutorService
接口,它可以在指定的延迟时间后或按固定的时间间隔执行任务。
下面是一个示例代码,它创建一个大小为5的定时任务线程池,并使用该线程池执行5个延迟任务和5个周期性任务:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
// 创建一个大小为5的定时任务线程池
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
// 提交5个延迟任务给线程池执行
for (int i = 0; i < 5; i++) {
executor.schedule(new DelayedTask(i), 1, TimeUnit.SECONDS);
}
// 提交5个周期性任务给线程池执行
for (int i = 0; i < 5; i++) {
executor.scheduleAtFixedRate(new PeriodicTask(i), 0, 1, TimeUnit.SECONDS);
}
// 关闭线程池
executor.shutdown();
}
}
class DelayedTask implements Runnable {
private int taskId;
public DelayedTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Delayed task #" + taskId + " is running.");
}
}
class PeriodicTask implements Runnable {
private int taskId;
public PeriodicTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Periodic task #" + taskId + " is running.");
}
}
在这个示例代码中,我们使用Executors.newScheduledThreadPool(5)
创建一个大小为5的定时任务线程池。然后,我们提交5个延迟任务和5个周期性任务给线程池执行,每个任务都是一个DelayedTask
或PeriodicTask
对象,实现了Runnable
接口中的run()
方法。其中,DelayedTask
表示延迟任务,它会在1秒后执行;PeriodicTask
表示周期性任务,它会每隔1秒执行一次。最后,我们调用executor.shutdown()
关闭线程池。
8.5 创建工作窃取线程池
工作窃取线程池是一种多线程执行模型,它可以提高线程的利用率和执行效率。在Java中,可以使用ForkJoinPool
类创建一个工作窃取线程池。
工作窃取(Work Stealing)是一种多线程执行模型,它用于解决多线程任务执行时任务负载不均衡的问题。在该模型中,每个线程都有一个自己的任务队列,当线程执行完自己的任务后,它可以从其他线程的任务队列中窃取任务执行,以此来提高线程的利用率和执行效率。
工作窃取模型通常用于处理分治任务,例如在并行计算中,将一个大的任务分成若干个小的子任务,每个子任务可以在不同的线程中并行执行。线程在执行自己的任务时,如果发现自己的任务队列为空,就可以从其他线程的任务队列中窃取任务执行,以此来保证线程的负载均衡。
举例说明
假设有一个餐馆,里面有4个服务员和一个食物出品口。顾客点餐后,服务员会将订单写在自己的便签上,并将便签放在自己的任务队列中。食物出品口会准备食物,并将食物放在一个共享的盘子中。当一个服务员完成自己的任务后,他就会从盘子中窃取一个食物并送到对应的桌子上。
如果服务员们都在等待食物出品口的食物,那么有些服务员可能会比其他服务员更快地完成自己的任务,然后就会处于空闲状态,而其他服务员仍在等待食物。这样会导致一些服务员的利用率很低,而其他服务员的利用率很高。
下面是一个示例代码,它创建一个大小为4的工作窃取线程池,并使用该线程池执行一个分治任务:
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class ForkJoinPoolExample {
public static void main(String[] args) {
// 创建一个大小为4的工作窃取线程池
ForkJoinPool pool = new ForkJoinPool(4);
// 提交一个分治任务给线程池执行
int[] array = new int[1000];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
int result = pool.invoke(new SumTask(array, 0, array.length - 1));
// 输出结果
System.out.println("Sum of array is " + result);
// 关闭线程池
pool.shutdown();
}
}
class SumTask extends RecursiveTask<Integer> {
private static final int THRESHOLD = 100;
private int[] array;
private int start;
private int end;
public SumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= THRESHOLD) {
int sum = 0;
for (int i = start; i <= end; i++) {
sum += array[i];
}
return sum;
} else {
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid + 1, end);
leftTask.fork();
rightTask.fork();
Integer leftResult = leftTask.join();
Integer rightResult = rightTask.join();
return leftResult + rightResult;
}
}
}
在这个示例代码中,我们使用new ForkJoinPool(4)
创建一个大小为4的工作窃取线程池。然后,我们创建一个长度为1000的整数数组,并使用SumTask
类表示一个求和的分治任务,它会将数组分成若干个子任务,每个子任务负责计算一部分数组元素的和。如果子任务的大小小于等于100,就直接计算;否则,就将任务分成两个子任务并提交给线程池执行,最后将子任务的结果合并。
我们将任务提交给线程池执行,并通过pool.invoke()
方法同步等待任务执行完成,获得任务的结果。最后,我们输出结果并关闭线程池。
9. 单线程与多线程
Java单线程和多线程各有优缺点,具体如下:
单线程的优点:
- 简单易用:单线程程序的实现和调试都比多线程程序简单。
- 避免线程竞争:单线程程序不需要考虑线程安全问题,不需要加锁或同步等操作。
- 易于调试:单线程程序的调试更容易,因为线程之间没有交互,不会出现死锁、竞争等问题。
单线程的缺点:
- 性能瓶颈:单线程无法利用多核处理器的优势,仅能使用一个处理器核心,性能受限。
- 响应时间长:当单个任务需要执行的时间较长时,程序的响应时间会变长,用户体验较差。
- 不适用于并行化:单线程无法将任务分解为多个并行执行的子任务,无法利用多线程的优势。
多线程的优点:
- 提高程序性能:多线程可以利用多个处理器核心并行处理多个任务,提高程序的执行效率和性能。
- 提高响应速度:多线程可以将任务分解为多个子任务并行执行,减少单个任务的执行时间,提高程序的响应速度。
- 支持并发编程:多线程可以支持并发编程,实现并发处理、异步编程等功能。
多线程的缺点:
-
线程安全问题:多线程程序需要考虑线程安全问题,需要加锁、同步等操作,增加了编程难度和代码复杂度。
-
调试难度:多线程程序的调试较为困难,因为线程之间存在交互和竞争,容易出现死锁、竞争等问题,调试难度较大。
-
容易产生性能问题:多线程程序需要考虑线程切换、锁竞争等问题,如果设计不当,容易产生性能问题。
-
可能会出现死锁等问题:多线程程序可能会出现死锁、饥饿等问题,需要谨慎设计和编写。
10.变量线程安全
方法有线程安全,变量也有线程安全,可以使用ThreadLocal
ThreadLocal
是Java中的一个线程封闭技术,它提供了一种简单的方式,让每个线程都可以独立地使用一个变量,而不会受到其他线程的影响。
具体来说,ThreadLocal
是一个线程本地变量,每个线程都有一个独立的ThreadLocal
实例,可以通过get和set方法来访问它的值。当多个线程同时访问同一个ThreadLocal
时,它们各自访问的是自己线程中的变量副本,而不会影响其他线程中的变量。
ThreadLocal
常用于解决多线程并发访问共享变量的线程安全问题。例如,在一个Web应用程序中,每个请求都会由一个独立的线程来处理,如果多个请求同时访问同一个共享变量,可能会出现线程安全问题。这时可以使用ThreadLocal
来将共享变量封装成ThreadLocal
对象,每个线程都可以独立地访问自己的变量副本,从而避免线程安全问题。
ThreadLocal
的使用主要如下:
- 线程安全的日期格式化工具类。
- 保存当前用户信息等数据,避免在方法间频繁传递。
- 在框架中使用,例如Spring中的事务管理就使用了
ThreadLocal
来管理事务状态。 - 分页
以下是一个使用ThreadLocal
实现分页的示例:
public class PageUtil {
// 定义一个ThreadLocal变量,用于保存分页信息
private static final ThreadLocal<PageInfo> pageInfoThreadLocal = new ThreadLocal<>();
/**
* 设置当前线程的分页信息
*
* @param pageNum 当前页码
* @param pageSize 每页数据量
*/
public static void setPageInfo(int pageNum, int pageSize) {
PageInfo pageInfo = new PageInfo(pageNum, pageSize);
pageInfoThreadLocal.set(pageInfo);
}
/**
* 获取当前线程的分页信息
*
* @return 分页信息
*/
public static PageInfo getPageInfo() {
return pageInfoThreadLocal.get();
}
/**
* 清除当前线程的分页信息
*/
public static void clearPageInfo() {
pageInfoThreadLocal.remove();
}
}
在上述示例中,PageUtil
类使用ThreadLocal
来保存当前线程的分页信息。具体来说,它定义了一个名为pageInfoThreadLocal
的ThreadLocal
变量,用于保存分页信息;并提供了三个方法:setPageInfo
、getPageInfo
和clearPageInfo
。
其中,setPageInfo
方法用于设置当前线程的分页信息,它创建一个PageInfo
对象,并将其保存到ThreadLocal
变量中。getPageInfo
方法用于获取当前线程的分页信息,它从ThreadLocal
变量中获取PageInfo
对象,并返回它。clearPageInfo
方法用于清除当前线程的分页信息,它调用ThreadLocal
的remove方法来清除ThreadLocal
变量中的数据。
使用示例如下:
public class UserService {
private UserDao userDao = new UserDao();
public List<User> getUserList(int pageNum, int pageSize) {
// 设置分页信息
PageUtil.setPageInfo(pageNum, pageSize);
try {
// 查询用户列表
return userDao.queryUserList(pageNum, pageSize);
} finally {
// 清除分页信息
PageUtil.clearPageInfo();
}
}
}
在上述示例中,UserService
类使用PageUtil
类来保存分页信息。具体来说,它在getUserList
方法中调用PageUtil.setPageInfo
方法来设置分页信息,然后调用UserDao.queryUserList
方法查询用户列表。最后,在finally块中调用PageUtil.clearPageInfo
方法清除分页信息。