当前位置:首页 > 后端开发 > 正文内容

JUC 多线程并发编程

邻居的猫1个月前 (12-09)后端开发1122

一、根本概念

1. 进程与线程

进程(Process):计算机中正在运转的程序的实例,是操作体系分配资源的根本单位。每个进程具有自己的内存空间、文件描绘符、数据栈等。

线程(Thread):进程中的一个履行单元。一个进程中至少有一个线程,一般称为主线程。线程是 CPU 调度和履行的最小单位。 线程同享进程的资源,一个进程中的多个线程能够并发履行,线程之间的通讯比进程之间的通讯更高效。

2. 并发与并行

并发(Concurrency):体系能够在同一时间段内处理多个使命,但这些使命或许并纷歧起履行。每个使命轮询持有cpu的时间片,在时间片内能够运用cpu,犹疑时间片很短,微观上看是并行履行。

并行(Parallelism):在同一时间有多个使命(线程、协程、事情驱动...)一起履行。一般需求多个 CPU 或多核处理器支撑,能够在物理上一起履行多个使命。

在实践中往往是并行和并发一起存在。

在这儿刺进图片描绘

3. 同步与异步

同步(Synchronous):使命之间的履行是依照次序进行的,一个使命有必要等候另一个使命完结后才干开端履行。

异步(Asynchronous):使命之间的履行不需求等候,能够在使命履行的一起进行其他操作。使命之间能够独立进行,不需求等候。

留意:同步在多线程中是指 在多个线程拜访同享资源时,经过某种机制来确保同一时间只需一个线程能够拜访该资源,以避免数据纷歧致或竞赛条件

4. 预备作业

引进下面依靠,查看更多输出信息

<dependency>
   <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.5.11</version>  <!-- 或许运用其他版别 -->
</dependency>

resources下创立一个 logback.xml

<configuration>
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- 只输出时间和线程称号 -->
            <pattern>%d{HH:mm:ss.SSS} [%thread] - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="console" />
    </root>
</configuration>
private static final Logger logger = LoggerFactory.getLogger(Main.class);

二、java的线程

1. 创立线程

  1. 承继 Thread 类并重写 run() 办法来界说线程的履行逻辑。然后,经过调用 start() 办法来发动线程。

    Thread t1 = new Thread("threadName") {	// 这儿咱们经过匿名类创立线程。(匿名类在创立时一起界说和实例化)
       @Override
       public void run() {
           System.out.println("hello world");
       }
    };
    
    t1.start();  // 发动 threadName 线程
    
  2. 完结 Runnable 接口,重写 run() 办法来界说线程要履行的代码。然后将该 Runnable 完结传递给一个 Thread 目标,调用 start() 发动线程。

    class Work1 implements Runnable {
        @Override
        public void run() {
            System.out.println("hello world");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Runnable work1 = new Work1();
            Thread t1 = new Thread(work1, "threadName");	// 创立线程
            t1.start();
        }
    }
    

    下面是经过 Lambda 表达式优化创立进程

    Runnable work1 = () -> System.out.println("hello world");
    Thread t1 = new Thread(work1, "threadName");
    t1.start();
    // Thread t1 = new Thread(() -> System.out.println("hello world"), "threadName");
    // t1.start();
    
  3. FutureTask 是一个既能够作为使命履行器又能够获取使命成果。它完结了 RunnableFuture 接口,因而能够用来创立和办理线程使命,一起能够获取使命的履行成果。

    // 创立使命
    FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            System.out.println("hello world");
            Thread.sleep(1000);
            return 1;
        }
    });
    
    // 创立线程并且使命
    Thread t1 = new Thread(task, "t1");
    t1.start();
    
    Integer returnValue = task.get();   // 会堵塞在这儿,知道获取到回来值
    System.out.println(returnValue);
    

2. 常用办法

办法名 描绘
start() 发动线程,使线程进入 安排妥当状况,由 CPU 调度运转。
run() 线程的履行逻辑,一般不直接调用,而是经过 start() 直接调用,调用 run() 不会敞开新线程。
sleep(long millis) (静态办法) 使当时线程休眠指定的时间(毫秒),在此期间不会占用 CPU。(堵塞状况)
join() 等候线程履行完结后再持续履行后续代码。
interrupt() 中止线程的履行,设置中止状况,但线程是否中止取决于代码逻辑(需手动检测或处理中止状况)。
isInterrupted() 查看线程是否被中止(不中止状况时回来 false)。
interrupted() (静态办法) 回来当时线程是否为中止状况,并把中止状况改为 false
currentThread() (静态办法) 获取当时履行的线程目标。
getName() / setName(String) 获取或设置线程的称号,用于调试或标识线程。
getId() 获取线程的仅有 ID。
setPriority(int) 设置线程优先级(取值规模 1~10,默认值 5,优先级高的线程更简单被 CPU 调度)。
getPriority() 获取线程优先级。
isAlive() 查看线程是否还在运转状况。
yield() (静态办法) 提示线程调度器让出 CPU,使当时线程由 运转状况 转为 安排妥当状况(或许会再次被当即调度)。
setDaemon(boolean) 设置线程为看护线程(后台线程),当一切非看护线程完毕时,看护线程主动停止。有必要在线程发动前调用。
getState() 回来线程的当时状况,成果为 Thread.State 枚举类型,或许的状况包括:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED

1. 根本办法的运用:

下面代码用不同办法创立了两个线程、设置了线程名 setName,发动线程 start,经过 Thread.sleep 让线程堵塞,经过 join 办法堵塞等候线程履行完毕

public class Main {
    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                logger.debug("t1 start");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t1");


        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    logger.debug("t2 start");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        t2.setName("t2"); // 设置线程名

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

        logger.debug("主线程在作业");

        t2.join(5000);  // 最多等候5秒, 由于t2线程实践大约只需2秒,所以这个2秒就不堵塞了
        logger.debug("t2 join end");

        t1.join();  // 堵塞等候t1线程完毕
        logger.debug("t1 join end");
    }
}
// 输出, 调查输出能够看到,由于t2.join堵塞main线程,即便t1比t2先履行完,也无法输出
10:21:38.202 [t1] - t1 start
10:21:38.202 [t2] - t2 start
10:21:38.202 [main] - 主线程在作业
10:21:40.208 [main] - t2 join end
10:21:40.208 [main] - t1 join end

在这儿刺进图片描绘

2. interrupt 的运用

interrupt 并不会让打断线程,而是让 isInterrupted 为 true,可是对处于在 sleep 或 wait 的线程(也便是等候的线程),并不会让 isInterrupted 为 true,而是抛出一个 InterruptedException 反常

调查下面代码能够看到 t1,t2的 isInterrupted 都是 false,t3的为 true。这儿看不懂t2的代码不要紧,下面会讲,只需理解上面那句话就能够。

public class Main {
    private static final Logger logger = LoggerFactory.getLogger(Main.class);
    static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
    	// 创立线程 t1
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread curThread = Thread.currentThread();
                logger.debug("t1 isInterrupted = {}", curThread.isInterrupted());
            }
        }, "t1");
		
		// 创立线程 t2
        Thread t2 = new Thread(() -> {
            synchronized (obj) {
                try {
                    obj.wait(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread curThread = Thread.currentThread();
                    logger.debug("t2 isInterrupted = {}", curThread.isInterrupted());
                }
            }
        }, "t2");

        Thread t3 = new Thread(() -> {
            while (true) {
                boolean isInter = Thread.currentThread().isInterrupted();
                if (isInter) {
                    logger.debug("t3 isInterrupted = true");
                    break;
                }
            }
        }, "t3");

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

        Thread.sleep(1000);

        logger.debug("interrupt thread");
        t1.interrupt();	// 打断t1
        t2.interrupt();	// 打断t2
        t3.interrupt();	// 打断t3
    }
}
// 输出
11:04:51.551 [main] - interrupt thread
11:04:51.552 [t3] - t3 isInterrupted = true
11:04:51.552 [t2] - t2 isInterrupted = false
11:04:51.553 [t1] - t1 isInterrupted = false

3. 看护线程

一般线程发动后一定会履行完整个线程中的代码,只需存在一般线程没有履行完,整个项目就不会完毕。而看护线程却不是这样,看护线程纷歧定会履行完整个线程中的代码,当一切的一般线程都履行完后,即便看护线程还有未履行的代码,它也会被 JVM 强制停止。

看护线程一般用于后台的辅佐使命,例如废物收回(GC)线程、守时使命等。

  1. 一般线程
    调查下面的输出能够看到,即便main线程的代码现已履行完了,可是会等候t1线程履行完,然后整个使用完毕
    public class Main {
        private static final Logger logger = LoggerFactory.getLogger(Main.class);
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                logger.info("t1 线程");
            }, "t1");
            t1.start();
    
            Thread.sleep(1000);
            logger.info("履行完结");
        }
    }
    // 输出
    20:39:32.587 [main] - 履行完结
    20:39:34.587 [t1] - t1 线程
    
  2. 看护线程
    当main线程履行完后,看护线程t1 也完毕,没有输出 "t1 线程"。
    public class Main {
        private static final Logger logger = LoggerFactory.getLogger(Main.class);
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                logger.info("t1 线程");
            }, "t1");
            t1.setDaemon(true); // 设置t1为看护线程
            t1.start();
    
            Thread.sleep(1000);
            logger.info("履行完结");
        }
    }
    // 输出
    20:42:02.762 [main] - 履行完结
    

3. 线程的状况(java)

上面线程常用办法讲到 getState() 会回来java线程的6种状况,下面阐明下各个状况。

New:线程目标已被创立,但没有调用了 start() 办法

Runnable:线程已发动,并且能够被调度履行,处于可运转状况。线程处于 RUNNABLE 状况时,或许正在履行,也或许处于操作体系的安排妥当行列中等候 CPU 时间片分配。

Blocked:当线程企图拜访某个目标的锁,而该锁现已被其他线程持有时,会从安排妥当行列移除,进入等候行列(堵塞行列),直到获取锁,堵塞状况下不会占用cpu,也不会分到时间片。但会进入等候行列时会有线程上下文切换会带来额定的 CPU 开支

Waiting:线程因某些条件不满足而无期限挂起,直到它被唤醒。一般经过 Object.wait()Thread.join() 进入此状况。等候状况下不会占用cpu,也不会分到时间片。

Timed Waiting:线程进入此状况是为了等候某些条件产生,但与等候状况不同,超时等候是有时间约束的。一般经过 Thread.sleep(long millis)Object.wait(long timeout)Thread.join(long millis) 等办法进入超时等候状况。不占用cpu

Terminated:线程在履行完一切使命后进入 TERMINATED 状况,意味着线程已完结履行,或许因反常退出。

一些细节问题和证明:

  1. java线程的 Runnable 状况包括 调用start办法后进入安排妥当行列后、占用cpu时、文件的读取时。尽管文件的读取在体系层面上看是堵塞的,可是java以为读取文件时依然是 Runnable 状况

    下面代码主线程会一向打印 t1 线程的状况,t1线程在 sleep 1秒后,读取文件。可是调查代码的输出能够看到 Timed WaitingTerminated 之间只需 Runnable 状况,并没有 Blocked 状况。能够证明读取文件的线程成语 Runnable 状况

    public class Main {
        private static final Logger logger = LoggerFactory.getLogger(Main.class);
    
        public static void main(String[] args) throws InterruptedException, IOException {
            Thread t1 = new Thread(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
    
                Path path = Paths.get("test.txt");
                try {
                    byte[] bytes = Files.readAllBytes(path);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
                logger.info("t1 线程");
            }, "t1");
    
            t1.start();
            Path path = Paths.get("out.txt");
            while (true) {
                System.out.println(t1.getState());
                Files.write(path, t1.getState().toString().getBytes(), StandardOpenOption.APPEND);
            }
        }
    }
    // 输出
    TIMED_WAITING
    TIMED_WAITING
    RUNNABLE
    RUNNABLE
    RUNNABLE
    ...(满是RUNNABLE)
    RUNNABLE
    TERMINATED
    TERMINATED
    

三、多线程常见内容

临界区:在并发编程中,一段需求拜访同享资源的代码区域,并且这个代码区存在多个线程的写操作。

竞态条件:在并发程序中,多个线程并发拜访同享资源时,在没有正确同步操控的状况下并发地拜访同享资源,然后导致不行猜测的成果。

1. synchronized

调查下面代码,咱们在两个线程中一起拜访 静态变量 cnt,并对它进行写操作,导致输出成果并不是预期的0,是不确定的。

这是由于 cnt ++ 这个操作不具有原子性。cnt ++ 被jvm编译成字节码时有四步操作,分别是 getstatic (获取cnt),iconst_1 (预备常量1),iadd (履行 cnt + 1),putstatic (把cnt + 1赋值给cnt)。而这四个过程或许在履行期间被打断,比方时间片缺乏线程切换。

public class Main {
    private static final Logger logger = LoggerFactory.getLogger(Main.class);
    static int cnt = 0;

    public static void main(String[] args) throws InterruptedException, IOException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                cnt++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                cnt--;
            }
        });
        
        t1.start();t2.start();

        t1.join();t2.join();
        System.out.println(cnt);
    }
}
// 输出
26026

1.1 synchronized 的概念

synchronized 关键字用于润饰代码块或办法。被润饰的代码块或办法需求传入一个目标锁,synchronized 能够确保同一时间只需一个线程能够获取到这个锁,其他没有获取到这个锁的线程会进入堵塞状况,只需获取到这个锁的才干够履行这个代码块,履行完代码块后会主动开释这个锁,确保了代码块的原子性。能够处理竞态条件的问题。

1.2 synchronized 润饰代码块

引进 synchronized 润饰代码块 cnt ++ 和 cnt --,传入的锁是一个静态变量 lock,线程t1和t2同享一个 lock, 所以t1和t2线程不会一起履行代码块中的内容。

public class Main {
    private static final Logger logger = LoggerFactory.getLogger(Main.class);
    static int cnt = 0;
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException, IOException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                synchronized (lock) {	// 确保 cnt ++ 的原子性
                    cnt++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                synchronized (lock) {	// 同一时间只会有一个线程获取到 lock 这个锁
                    cnt--;
                }
            }
        });

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

        t1.join();t2.join();
        System.out.println(cnt);
    }
}
// 输出
0

1.3 synchronized 润饰办法

synchronized 用于办法上时,分为两种,实例办法和静态办法

实例办法

当 synchronized 用于实例办法时,它会 锁住调用该办法的实例目标。也便是说,同一个类的不同线程对同一个目标调用同步实例办法时,会排队等候履行,不能一起履行该办法。

当我多个线程都用 t1 这个实例调用 test 时,会排队等候履行。由于实例办法上加上synchronized 相当于锁住 当时实例 this

Test t1 = new Test();
t1.test();

class Test {
    public synchronized void test() {
    }
// 上面代码相当于下面注释这个,锁住的是 this, 这个示例中锁住的是 t1 这个实例
//    public void test() {
//        synchronized (this) {
//        }
//    }
}
静态办法

当 synchronized 用于静态办法时,它会 锁住类的 Class 目标(class目标对一切实例都同享,只需一份),即锁住整个类的类等级的资源,一切实例同享同一个锁。

当我多个线程都经过 Test.test() 调用时,会排队等候履行。

Test.test();

class Test {
    public static synchronized void test() {
    }
// 上面代码相当于下面注释这个,锁住的是Class目标
//    public static void Test() {
//        synchronized (Test.class) {
//        }
//    }
}

2. 线程安全性

在 Java 中,线程安全性一般是 多个线程 同享拜访同一资源时,不会导致数据过错或程序溃散。

1. 线程不安全的原因

线程不安全一般是由于以下原因引起的:

  • 竞态条件(Race Condition)
    竞态条件产生在多个线程一起拜访同享资源时,且至少有一个线程会修正资源。这时不同线程的履行次序或许会导致纷歧致的成果。

  • 不行见性(Visibility)
    一个线程对同享变量的修正或许在其他线程中不行见。由于线程或许会缓存局部变量或同享变量的副本,导致线程间的修正不能及时传达。

  • 原子性(Atomicity)
    原子性是指操作在履行进程中不能被中止。多个线程拜访同享资源时,假如没有恰当的同步机制,或许会导致非原子的操作,从而引发纷歧致的成果。

2. 引证类型局部变量线程安全性剖析

list 线程不安全,会呈现下标越界。由于有两个线程都在用 test 实例的 test1办法对同一个 list 目标进行修正,不同线程的履行次序导致了删去履行的次数比添加多,呈现越界。(竞态条件、原子性)

public class Main {
    public static void main(String[] args) {
        Test test = new Test();

        new Thread(() -> test.test1()).start();
        new Thread(() -> test.test1()).start();
    }
}

class Test {
    List<Integer> list = new ArrayList<>();
    void test1() {
        for (int i = 0; i < 10000; i++) {
            test2();
            test3();
        }
        System.out.println(list.size());
    }

    void test2() {
        list.add(1);
    }
    void test3() {
        list.remove(0);
    }
}

list 线程安全。尽管同一个实例 test 的两个线程都在对 list 进行修正,可是 list 是局部变量,当调用test1时会创立新的list变量,两个线程修正的list不是同一个

public class Main {
    public static void main(String[] args) {
        Test test = new Test();

        new Thread(() -> test.test1()).start();
        new Thread(() -> test.test1()).start();
    }
}

class Test {
    void test1() {
        List<Integer> list = new ArrayList<>();	// 不同点
        for (int i = 0; i < 10000; i++) {
            test2(list);
            test3(list);
        }
        System.out.println(list.size());
    }

    void test2(List<Integer> list) {
        list.add(1);
    }

    void test3(List<Integer> list) {
        list.remove(0);
    }
}

list 线程不安全。尽管 main 线程中创立的两个线程的 list 变量是两个不同的,可是test1中调用test3办法时把list传递了曩昔,test3创立了新的线程,这个新的线程同享了list,并且进行了修正。(竞态条件、原子性)

public class Main {
    public static void main(String[] args) {
        Test test = new Test();

        new Thread(() -> test.test1()).start();
        new Thread(() -> test.test1()).start();
    }
}

class Test {
    void test1() {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            test2(list);
            test3(list);
        }
        System.out.println(list.size());
    }

    void test2(List<Integer> list) {
        list.add(1);
    }

    void test3(List<Integer> list) {
        new Thread(() -> list.remove(0)).start();
    }
}

3. 传统线程安全的类

传统线程安全的类一般包括 经过 synchronized 关键字来确保线程安全和经过不行变的性质,确保多个线程中不会呈现并发修正的状况

1. 经过 synchronized 来确保线程安全

扫描二维码推送至手机访问。

版权声明:本文由51Blog发布,如需转载请注明出处。

本文链接:https://www.51blog.vip/?id=127

标签: 后端
分享给朋友:

“JUC 多线程并发编程” 的相关文章

【日记】咱们行发工资真的便是 Black Box……(577 字)

【日记】咱们行发工资真的便是 Black Box……(577 字)

正文 今日头好油…… 昨日应付完了真实太晚,就没洗澡。现在的头几乎无法看…… 回想了一下,今日如同什么都没干。字面意义上的。今日新行长下来,带了一堆东西。去帮了忙。他看见我还一愣。估量是头太油了……. 发工资了。市分行的搭档问我怎样比跟我同一批进来的人高那么多。你问我我也不知道啊…… 人力也不发个工...

c语言数组定义和赋值,C语言数组定义与赋值详解

c语言数组定义和赋值,C语言数组定义与赋值详解

定义数组 一维数组```c// 定义一个整型数组,包含10个元素int arr;``` 二维数组```c// 定义一个整型二维数组,包含3行4列int matrix;``` 初始化数组 一维数组```c// 初始化一个整型数组int arr = {1, 2, 3, 4, 5};``` 二维数组``...

java和python,编程语言的选择与未来展望

1. 用途: Java:通常用于企业级应用、Android 应用开发、大型系统开发等。 Python:广泛用于数据分析、机器学习、Web 开发、自动化脚本等。2. 语法: Java:语法相对严格,需要明确声明变量类型,并且使用分号作为语句的结束符。 Python:语法简洁明了...

java算法,基础概念与常用算法解析

java算法,基础概念与常用算法解析

Java是一种广泛使用的高级编程语言,用于开发各种应用程序,包括桌面应用程序、Web应用程序、移动应用程序和游戏等。在Java中实现算法时,通常需要遵循一定的步骤和最佳实践,以确保代码的效率、可读性和可维护性。1. 理解算法:在开始编码之前,确保你完全理解了算法的工作原理。这包括理解算法的输入、输出...

php如何安装,从入门到环境搭建

php如何安装,从入门到环境搭建

安装PHP是一个多步骤的过程,通常取决于您正在使用的操作系统。以下是在不同操作系统上安装PHP的基本步骤: Windows1. 下载PHP: 访问下载PHP。 选择与您的Windows版本兼容的版本。2. 安装PHP: 双击下载的`.msi`文件启动安装程序。 按照提示完成安...

配置java环境变量

配置Java环境变量通常包括设置`JAVA_HOME`环境变量、`PATH`环境变量以及`CLASSPATH`环境变量。以下是在Windows系统上配置Java环境变量的步骤:1. 下载并安装Java: 访问Oracle官方网站下载Java Development Kit 。 安装JDK...