(一)多线程基础

多线程执行过程

主线程:

public class TestThread {

    public static void main(String[] args) {
        MyThread mt = new MyThread("小强");
        mt.start();
        for (int i = 0; i < 20000 ; i++) {
            System.out.println("旺财"+i);
        }
    }
}

自定义线程

public class MyThread extends Thread{

    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 20000; i++) {
            String name = Thread.currentThread().getName();
            System.out.println(name + i);
        }
    }
}

程序启动运行 main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。
通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来讲一讲原理。
多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

在这里插入图片描述

当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

创建线程的方法一

继承Thread,如上的代码所示

创建线程的方法二

实现Runable接口

public class MyRunable implements Runnable{
    
    @Override
    public void run() {
        for (int i = 0; i < 20000; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}
public class TestThread {

    public static void main(String[] args) {
        MyRunable myRunable = new MyRunable();
        Thread th = new Thread(myRunable, "小强");
        th.start();
        for (int i = 0; i < 20000 ; i++) {
            System.out.println("旺财"+i);
        }
    }
}

通过实现 Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程
代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ABNlp91c-1644373436090)(../img/image-20210809100607456.png)]

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread
对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现
Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程
编程的基础。

tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。
而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

创建线程的方法三

实现Callable接口

public class TestNewIo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
 
        ThreadDemo td = new ThreadDemo();
 
        //1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
        FutureTask<Integer> result = new FutureTask<Integer>(td);
 
        new Thread(result).start();
 
        //2.接收线程运算后的结果
        Integer sum = result.get();  //FutureTask 可用于 闭锁 类似于CountDownLatch的作用,在所有的线程没有执行完成之后这里是不会执行的
        System.out.println(sum);
        System.out.println("------------------------------------");
    }
}
 
class ThreadDemo implements Callable<Integer> {
 
    @Override
    public Integer call() throws Exception {
 
        int sum = 0;
 
        for (int i = 0; i <= 10; i++) {
            sum += i;
            System.out.println(currentThread().getName()+i);
        }
 
        return sum;
    }
 
}

创建线程的方法四

参考:线程池

Runnable和Callable的区别

相同点
1、两者都是接口;
2、两者都可用来编写多线程程序;
3、两者都需要调用Thread.start()启动线程;

不同点
1、两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
2、Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

注意点
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

Thread 和Runnable的区别

原文

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。

  2. 可以避免java中的单继承的局限性。

  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

扩充:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用
java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进
程。

使用匿名内部类的方式实现Runable接口

public class TestThread {

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        };
        new Thread(runnable,"小强").start();
        System.out.println("主线程");
    }
}
并发与并行

并发:单线程交替执行多任务

并行:多线程同时执行多任务

同步与异步

同步,就是调用某个东西是,调用方得等待这个调用返回结果才能继续往后执行。

异步,和同步相反 调用方不会理解得到结果,而是在调用发出后调用者可用继续执行后续操作,被调用者通过状体来通知调用者,或者通过回掉函数来处理这个调用

阻塞与非阻塞

阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。

非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

线程安全问题

多个线程并发访问同一个资源就容易出现线程安全问题

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写
操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步
否则的话就可能影响线程安全。

线程同步

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

实现同步的三种方法:

  1. 同步代码块
  2. 同步方法
  3. 锁机制
同步代码块

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:

synchronized(同步锁){
     需要同步操作的代码
}

同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着
(BLOCKED)。

同步方法

使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

public synchronized void method(){
   可能会产生线程安全问题的代码 
}
同步锁

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:

public void lock() :加同步锁。
public void unlock() :释放同步锁。

public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            //业务代码
        }finally {
            lock.unlock();
        }
        
        System.out.println("主线程");
    }

这里用到的Lock是一个接口,而ReentrantLock是它的实现,叫可重入锁,synchronized也是java的可重入锁

可重入锁:什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

// 演示可重入锁是什么意思,可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的
// 可重入降低了编程复杂性
public class WhatReentrant {
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (this) {
					System.out.println("第1次获取锁,这个锁是:" + this);
					int index = 1;
					while (true) {
						synchronized (this) {
							System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
						}
						if (index == 10) {
							break;
						}
					}
				}
			}
		}).start();
	}
}

一个故事理解可重入锁

线程五种状态详解

线程方法:

sleep() :

让线程进入睡眠状态,睡眠到期后进入可运行状态,而不是进入运行状态

wait() :

一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

public class TestThread {
    static Object object = new Object();

    public static void main(String[] args) {
        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        while (true){
                            synchronized (object){
                                try {
                                    System.out.println(Thread.currentThread().getName()+" === 获取到锁对象,调用wait方法,进入waiting状态,释放锁对象");
                                    object.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                System.out.println(Thread.currentThread().getName()+" === 从wait状态醒来,获取到锁对象继续执行");
                            }
                        }
                    }
                },"等待线程"
        ).start();

        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        try {
                            System.out.println(Thread.currentThread().getName()+" === 等待3s");
                            Thread.sleep(3000);

                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        
                        //要唤醒则要先获得对象的锁
                        synchronized (object){
                            System.out.println(Thread.currentThread().getName()+" === 获取到锁对象,调用notify方法,释放锁对象");
                            object.notify();
                        }
                    }
                },"唤醒线程"
        ).start();
    }
}

通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。

其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

object.wait()是设置当前线程在该对象上等待,直到有线程调用obj.notify()方法(或notifyAll()方法)。当调用wait()方法后,该线程会进入一个等待队列,等待队列中可能有多个线程,notify()会随机唤醒其中一个线程,而notifyAll()会唤醒所有线程。
wait()和notify()方法必须在sychronized代码块中,调用这些方法时都需要先获得目标对象的一个监视器,然后调用这些方法时会释放监视器
与sleep不同的是,sleep()会一直占有所持有的锁,而wait()会释放锁。
————————————————

线程中断方法

public void interrupt();
public boolean isInterrupted();
public static boolean interrupted();

三个方法很相似,线程中断只是通知目标线程有人希望你退出,而并不是使目标线程退出。
第一个方法是通知目标线程中断,即设置目标线称的中断标志位;
第二个方法判断当前线程是否被中断,如果被中断(即中断标志位被设置),则返回true,否则返回false;
第三个方法判断当前线程的中断状态,并清除该线程的中断标志位(也就意味着,如果连续调用两次该方法,并且中间没有再次设置中断标志位,第二次会返回false,因为中断标志位已经被清除)。

设置优先级方法
public static void main(String[] args) {    Thread thread = new Thread(            () -> System.out.println("test"), "自定义线程"    );    thread.setPriority(Thread.MIN_PRIORITY);    thread.setPriority(Thread.MAX_PRIORITY);    thread.setPriority(Thread.NORM_PRIORITY);    thread.start();    System.out.println("主线程");}
等待线程(join)和谦让(yield)

public final void join() throws InterruptedException;
public static native void yield();

如果一个线程的执行需要另一个线程的参与(比如当前线程执行需要另一个线程执行完毕才能继续执行),这时候可以调用join()方法。t1.join()方法表示等待线程t1执行完毕之后,当前线程再继续执行。当然也可以给join()设置时间参数。

public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"开始执行");

        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()+"正在打印:"+i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"newThread");

        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"结束执行");
    }

在这里插入图片描述
可以看出它是利用wait方法来实现的,上面的例子当main方法主线程调用线程t的时候,main方法获取到了t的对象锁,而t调用自身wait方法进行阻塞,只要当t结束或者到时间后才会退出,接着唤醒主线程继续执行。millis为主线程等待t线程最长执行多久,0为永久直到t线程执行结束。可以主动指定时间

注:join()的本质是让调用线程wait()在当前线程对象实例上,其部分源码如下:

while (isAlive()) {   wait(0);}

当线程执行完毕后,它会让被等待的线程在退出前调用notifyAll()通知所有等待的线程继续执行。因此不要在Thread对象实例上使用类似wait()或者notify()等方法。
**yield()方法是使当前线程让出CPU,但该线程会再次抢夺CPU。**就是和同一优先级的线程一起抢夺CPU

在这里插入图片描述

守护线程

在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。 在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

最典型的守护线程有:JVM来及回收和异常处理

所谓守护 线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。在守护线程中创建的线程还是守护线程

创建守护线程

public static void main(String[] args) {    Thread thread = new Thread(            () -> System.out.println("test"), "守护线程"    );    thread.setDaemon(true); //必须启动前调用    thread.start();    System.out.println("主线程");}

Lambda表达式:创建接口对象

函数式接口,即只有一个方法的接口

public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+": myThread线程执行");
        },"myThread").start();
        System.out.println(Thread.currentThread().getName()+": main线程执行");
    }

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值