JavaSE自学笔记016_Real(多线程)

JavaSE自学笔记016_Real(多线程)

一、进程与线程

1、进程
一个正在执行中的程序叫做一个进程。系统会为了这个进程发配独立的【内存资源】,进程是程序的依次执行过程,他有着自己独立的生命周期,他会在启动程序的时候产生,运行程序的时候存在,关闭程序的时候消亡。
如果一个进程里由多个任务需要执行,一种方式是按照顺序执行,一个任务执行完再进行另一个任务,另一种方式是一起同时开始,最后一个执行完成结束。
第一种就是串行执行,第二种就是需要每一个任务分配类似交替执行。
如果每一个任务分配单独的进程执行,进程之间就必须能够通信,但是病毒会很容易通过一个进程来操作运行中的其他进程。
2、线程
线程是由进程创建的,是进程的一个实体,是具体干活的人,一个进程可能有多个线程,线程不独立分配内存,而是共享进程的内存资源,线程可以共享cpu的计算资源。
现在,进程更强调【内存资源额分配】,而线程更强调【计算资源的分配】,因为有了线程的概念,一个进程的线程就不能修改另一个线程的数据,隔离性更好,安全性更好。

二、创建线程的方法

1、继承Thread类实现run方法
步骤:
	(1)定义类继承Thread
	(2)重写类继承Thread类中的run方法,(目的:将自定义代码储存在run方法,让线程运行)
	(3)调用线程的start方法:(该方法有两步,启动线程,调用run方法)
// 定义线程类,并继承Thread类
public class UseThread extends Thread{
    // 重写run()方法
    @Override
    public void run() {
        System.out.println(2);
    }

    public static void main(String[] args) {
        System.out.println(1);
        new UseThread().start(); //开启线程
        System.out.println(3);
    }
}
2、实现Runnable接口
步骤:
	(1)创建任务:创建类实现Runnable接口
	(2)使用Thread为这个任务分配线程
	(3)调用线程的start方法
//定义接口类实现Runnable接口
public class UseRunnable implements Runnable{
    //重写run()方法
    @Override
    public void run() {
        System.out.println(2);
    }

    public static void main(String[] args) {
        System.out.println(1);
        // 创建Thread()对象,利用Runnable为参数的构造器传入UseRunnable类,调用start()方法
        new Thread(new UseRunnable()).start();
        System.out.println(3);
    }
    //可以直接使用匿名内部类的直接开启新线程
    public static void main(String[] args) {
        System.out.println(1);
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(2);
            }
        }).start();
        System.out.println(3);
    }
//还可以用箭头函数
    public static void main(String[] args) {
        System.out.println(1);
        new Thread(() -> System.out.println(2);).start();
        System.out.println(3);
    }

3、使用Lammbda表达式
//定义接口类实现Runnable接口
public class UseRunnable implements Runnable{
    //重写run()方法
    @Override
    public void run() {
        System.out.println(2);
    }

	//可以直接使用匿名内部类的直接开启新线程
    public static void main(String[] args) {
        System.out.println(1);
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(2);
            }
        }).start();
        System.out.println(3);
    }
	//还可以用箭头函数
    public static void main(String[] args) {
        System.out.println(1);
        new Thread(() -> System.out.println(2);).start();
        System.out.println(3);
    }
4、有返回值的线程
package com.ThreadStudy.CallableStudy;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class UseCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(3000);
        return 2;
    }


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new UseCallable());
        new Thread(futureTask).start();
        Integer integer = futureTask.get();
        System.out.println(integer);
    }
}

单线程与多线程执行效率对比

package com.ThreadStudy;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class TestThread1 implements Callable<Long> {

    private int from;
    private int to;

    public TestThread1() {
    }

    public TestThread1(int from, int to) {
        this.from = from;
        this.to = to;
    }

    @Override
    public Long call() throws Exception {
        long res = 0L;
        for (int i = from; i < to; i++) {
            res += i;
        }
        return res;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        long res = 0;
        for (int i = 0; i < 100000000; i++) {
            res += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("单线程用时:" + (end - start));
        System.out.println("单线程res = " + res);

        System.out.println("================");

        start = System.currentTimeMillis();
        res = 0L;
        FutureTask[] futureTasks = new FutureTask[5];
        for (int i = 0; i < 5; i++) {
            FutureTask<Long> Task = new FutureTask<>(new TestThread1(i*20000000, (i+1)*20000000));
            new Thread(Task).start();
            futureTasks[i] = Task;
        }
        for (int i = 0; i < 5; i++) {
            Long sum = (Long) futureTasks[i].get();
            res += sum;
        }
        end = System.currentTimeMillis();
        System.out.println("多线程用时:" + (end - start));
        System.out.println("多线程res = " + res);
    }
}

执行结果:
单线程用时:32
单线程res = 4999999950000000
================
多线程用时:16
多线程res = 4999999950000000
5、守护线程

Java 提供了两中类型的线程,用户线程和守护线程,守护线程旨在为用户线程提供服务,并且尽在用户线程运行的时候才需要。
1、守护线程的使用:
守护线程对于后台支持任务费城有用,比如垃圾回收,释放未使用的对象的内存以及从缓存中删除不需要的条目,大多数JVM线程都是守护线程,qq等聊天软件,主程序是非守护线程,所有的聊天窗口是守护线程,聊天过程中,直接关闭聊天应用程序的时候,聊天窗口会随之关闭。

2、创建守护线程
要将线程设置为守护线程,需要做的就是调用Thread.setDaemon()

package com.ThreadStudy;

public class DaemonStudy {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            int count = 10;
            Thread t2 = new Thread(() -> {
                while (true){
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("我是守护线程");
                }
            });
            t2.setDaemon(true); //将t2设置为t1的守护线程
            t2.start();

            while(count >= 0){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("我是用户线程");
                count --;
            }
            System.out.println("用户线程结束了=============");
        });
        //t1.setDaemon(true); // 将t1设置为main的守护线程
        t1.start();
    }
}

三、线程的生命周期

线程的生命周期包括从创建到终结的整个过程
在这里插入图片描述
在这里插入图片描述

线程阻塞函数:join()函数

package com.ThreadStudy;

public class TestJoin {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1---------------");
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2---------------");
        });

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

        //========join()方法的使用============
        t1.join(); //线程实例调用join()方法会阻塞t1被调用的所在线程
        //阻塞后会进行等待,只有等到t1执行完了,所在线程才能执行。
        System.out.println("main---------------------");
    }
}

三、JVM模型中存在的问题

在执行程序的时候,为了提高性能,编译器和处理器通常会对指令进行重新排序

1、指令重排
在指令重排中,有一个经典的as-if-serial语义,计算机会按照该语义及逆行优化,目的是不管怎么排序,程序执行的结果不能改变,为了遵循as-if-serial语义,编译器和处理器不会对有数据依赖的关系的操作做重新排序,以为会改变执行的结果。
package com.ThreadStudy;

public class OutOfOrderExecution {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
    private static int count = 0;

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for(;;){
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    x = b;
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            t1.start();
            t2.start();
            System.out.println("一起执行了:"+ (count++) + "次。");
            if(x == 0 && y == 0){
                long end = System.currentTimeMillis();
                System.out.println("耗时:" + (end-start) + "毫秒, (" + x + y + ")");
                break;
            }
        }
        a = 0;
        b = 0;
        x = 0;
        y = 0;
    }
}

解决指令重排的方式是使用内存屏障:
在Java语言中可以使用volatile关键字来保证在依次读写操作的时候避免指令重排,【内存屏障】是在我们的读写操作之前加入一条指令,当CPU碰到这条指令的时候,必须等待前面的指令执行完才能执行指令后边的指令。

(2)内存可见性
package com.ThreadStudy;

public class TestVolatile {

    //======volatile关键字的使用=========
    
    //public static boolean flag = false;
    public volatile static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while(!flag){}
            System.out.println("程序结束了");
        }).start();

        Thread.sleep(2000);
        flag = true;
    }
}

Thread线程一直在缓存中读取flag 的值,不能感知到主线程已经将flag的值修改了,这就是可见性的问题,解决方案就是利用关键字volatile来修饰出视的静态变量flag

happens-before语义:
JYM使用【happens-before】的概念来阐述操作之间的内存可见性,在JVM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
在Java 规范提案中为让大家理解内存可见性的这个概念,提出了happens-before的概念来阐述操作之间的内存可见性。对应Java程序员来说,理解happens-before是理解JMM的关键。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。as-if-serial语义保证单线程内程序的执行结果不被改变happens-before关系保证正确同步的多线程程序的执行结果不被改变。

(3 )线程争抢
package com.ThreadStudy;

public class ThreadCompetition {
    private static int count = 0;
    // synchronized关键字加上之后,该方法只能在一个线程执行完之后才能在另一个线程中被调用使用
    public synchronized static void adder(){
        count++;
    }
    
    /*
    *
    public static void adder(){
        count++;
    }*/

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    adder();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    adder();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最后的结果是:" + count);
    }
}
输出结果:
最后的结果是:17510

多个线程同时争抢相同的公共资源就会出现线程争抢,线程争抢会造成数据安全问题,最好的解决方案就是**【加锁】** 关键字:synchronized

synchronized关键字加上之后,该方法只能在一个线程执行完之后才能在另一个线程中被调用使用

四、线程安全的实现方法

(1)数据不可变
在java当中,一切不可变的对象(immutable)一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障的措施,比如final关键字修饰的基础数据类型,再比如说咱们的java字符串儿。只要一个不可变的对象被正确的构建出来,那外部的可见状态永远都不会改变,永远都不会看到他在多个线程之中处于不一致的状态,带来的安全性是最直接最纯粹的。比如使用final修饰的基础数据类型(引用数据类型不可以)、比如java字符串,而一旦被创建就永远不能改变,其实的谷歌的开发工具包(guava)中也给我们提供了一些不可变的一类(immutable)。
(2)互斥同步
互斥同步是常见的一种并发正确性的保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用,互斥是实现同步的一种手段,互斥是因、同步是果,互斥是方法,同步是目的。在Java中最基本的互斥同步手段,就是synchronized字段,除了synchronize的之外,我们还可以使用ReentrantLock等工具类实现。接下来我们就尝试学习java中的锁。
(3)非阻塞同步
互斥同步面临的主要问题是,进行线程阻塞和唤醒带来的性能开销,因此这种同步也被称为阻塞同步,从解决问题的方式上来看互斥同步是一种【悲观的并发策略】,其总是认为,只要不去做正确的同步措施,那就肯定会出现问题,无论共享的数据是否真的出现,都会进行加锁。这将会导致用户态到内核态的转化、维护锁计数器和检查是否被阻塞的线程需要被唤醒等等开销。
随着硬件指令级的发展,我们已经有了另外的选择,基于【冲突检测的乐观并发策略】。通俗的说,就是不管有没有风险,先进行操作,如果没有其他线程征用共享数据,那就直接成功,如果共享数据确实被征用产生了冲突,那就再进行补偿策略,常见的补偿策略就是不断的重试,直到出现没有竞争的共享数据为止,这种乐观并发策略的实现,不再需要把线程阻塞挂起,因此同步操作也被称为非阻塞同步,这种措施的代码也常常被称之为【无锁编程】,也就是咱们说的自旋。我们用cas来实现这种非阻塞同步。
(4)无同步方案
多个线程共享数据,但是这些数据仅仅只是在单独的线程进行计算,得出结果,而不能被其他线程所影响,如果能保证这一点,我们可以将共享数据的可见范围限制在一个线程之内,这样就可以无需同步,也能保证各个线程之间不会出现数据争抢的问题。
ThreadLocal泛型类可以提供储存变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的,通过get()和set()方法可以得到当前线程对应的值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仲子_real

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值