Java笔记-11 多线程

多线程

  • 进程是资源分配的单位,线程是调度和执行的单位。
  • 多线程的三种方式,继承Thread类,实现Runnable接口,实现Callable接口,前两种常用,第二个更常用,因为无法多继承,可以实现多个接口
  • 继承Thread类,重写run方法,构建对调用start方法
  • 实现Runnable接口,重写run方法,通过new Thread(对象).start()调用

Thread

  • Thread示例,start方法并不一定立即调用,由CPU调用,下面的例子每次调用输出的结果都不一致
/**
 * Thread多线程
 * 继承Thread,重写run方法,构建对象并调用start方法
 */
public class Test extends Thread{
    @Override
    public void run() {
        for(int i = 0;i<10;i++){
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        //创建对象
        Test t = new Test();
        //调用start方法
        t.start();
        //如果不用start而是调用run方法,那么还是单线程,先调用run再继续下面的
        //上面开启另一个线程后继续下面的线程
        for(int i = 0;i<10;i++){
            System.out.println("------"+i);
        }
    }
}
  • Thread示例2,多线程下载图片
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

public class PictureDownload extends Thread{
    private String url;
    private String name;
    public PictureDownload(String url,String name){
        this.url=url;
        this.name=name;
    }
    public void run(){
        DownloadURL d = new DownloadURL();
        d.download(url,name);
    }
    //main
    public static void main(String[] args) {
        //启动三个线程
        String url1="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582086332907&di=d4a320f4ca1c79d9352f41ff20b44f85&imgtype=0&src=http%3A%2F%2Fsem.g3img.com%2Fg3img%2Fntdljy888%2Fc2_20171019113746_28237.jpg";
        String target1 = "testdata/A/1.jpg";
        String url2="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582086332908&di=a5be915f708913cd6e0063e48b4acb8c&imgtype=0&src=http%3A%2F%2Fwww.znxkedu.com%2Fquxuecimg2016%2Fimage%2F20191023%2Fpxems_6201007.jpg";
        String target2="testdata/A/2.jpg";
        String url3="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582086332908&di=d7c6168f33b24c213279c0b0428e0317&imgtype=0&src=http%3A%2F%2Fqd.java.tedu.cn%2Fupload%2F20171013%2F20171013165916_450.jpg";
        String target3="testdata/A/3.jpg";
        PictureDownload pd1 = new PictureDownload(url1,target1);
        PictureDownload pd2 = new PictureDownload(url2,target2);
        PictureDownload pd3 = new PictureDownload(url3,target3);
        pd1.start();
        pd2.start();
        pd3.start();

    }
}

class DownloadURL{
    /**
     * 根据url下载到target文件
     * @param url
     * @param target
     */
    public void download(String url,String target){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(target));
            System.out.println(target+"..下载成功");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Runnable

  • 调用时使用new Thread(Runnable对象).start()方法,编写代码由extends Thread改为implements Runnable,其他相同
  • 应优先使用接口
    • Runnable不能抛出异常,返回值为void
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

public class  PicdownloadRunnable implements Runnable{
    private String url;
    private String name;
    public PicdownloadRunnable(String url,String name){
        this.url=url;
        this.name=name;
    }
    public void run(){
        DownloadURL d = new DownloadURL();
        d.download(url,name);
    }
    //main
    public static void main(String[] args) {
        //启动三个线程
        String url1="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582086332907&di=d4a320f4ca1c79d9352f41ff20b44f85&imgtype=0&src=http%3A%2F%2Fsem.g3img.com%2Fg3img%2Fntdljy888%2Fc2_20171019113746_28237.jpg";
        String target1 = "testdata/A/11.jpg";
        String url2="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582086332908&di=a5be915f708913cd6e0063e48b4acb8c&imgtype=0&src=http%3A%2F%2Fwww.znxkedu.com%2Fquxuecimg2016%2Fimage%2F20191023%2Fpxems_6201007.jpg";
        String target2="testdata/A/21.jpg";
        String url3="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582086332908&di=d7c6168f33b24c213279c0b0428e0317&imgtype=0&src=http%3A%2F%2Fqd.java.tedu.cn%2Fupload%2F20171013%2F20171013165916_450.jpg";
        String target3="testdata/A/31.jpg";
        PictureDownload pd1 = new PictureDownload(url1,target1);
        PictureDownload pd2 = new PictureDownload(url2,target2);
        PictureDownload pd3 = new PictureDownload(url3,target3);
        new Thread(pd1).start();
        new Thread(pd2).start();
        new Thread(pd3).start();
    }
}
  • Runnable共享资源,类似多线程抢票,需要考虑数据的准确性
public class TestTicket implements Runnable {
    private int tickets = 99;

    @Override
    public void run() {
        while (true){
            if(tickets<0){
                break;
            }
            try {
                Thread.sleep(100); //有网络延迟时,会出现负数
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"--->"+tickets--);
        }
    }

    public static void main(String[] args) {
        TestTicket tk = new TestTicket();
        //一份资源多个线程争夺
        new Thread(tk ,"1号线").start();
        new Thread(tk,"2号线").start();
        new Thread(tk,"3号线").start();
    }
}

Callable

  • 可以抛异常,有返回值
  • 并发用
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class PicdownloadCallable implements Callable<Boolean> {
    private String url;
    private String name;
    public PicdownloadCallable(String url, String name){
        this.url=url;
        this.name=name;
    }
    //重写call方法而不是run,需要返回值
    public Boolean call(){
        DownloadURL d = new DownloadURL();
        d.download(url,name);
        return true;
    }
    //main
    public static void main(String[] args) {
        //启动三个线程
        String url1="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582086332907&di=d4a320f4ca1c79d9352f41ff20b44f85&imgtype=0&src=http%3A%2F%2Fsem.g3img.com%2Fg3img%2Fntdljy888%2Fc2_20171019113746_28237.jpg";
        String target1 = "testdata/B/11.jpg";
        String url2="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582086332908&di=a5be915f708913cd6e0063e48b4acb8c&imgtype=0&src=http%3A%2F%2Fwww.znxkedu.com%2Fquxuecimg2016%2Fimage%2F20191023%2Fpxems_6201007.jpg";
        String target2="testdata/B/21.jpg";
        String url3="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582086332908&di=d7c6168f33b24c213279c0b0428e0317&imgtype=0&src=http%3A%2F%2Fqd.java.tedu.cn%2Fupload%2F20171013%2F20171013165916_450.jpg";
        String target3="testdata/B/31.jpg";
        PicdownloadCallable pd1 = new PicdownloadCallable(url1,target1);
        PicdownloadCallable pd2 = new PicdownloadCallable(url2,target2);
        PicdownloadCallable pd3 = new PicdownloadCallable(url3,target3);
        //创建执行服务
        ExecutorService service = Executors.newFixedThreadPool(3);
        //提交执行
        Future<Boolean> result1 = service.submit(pd1);
        Future<Boolean> result2 = service.submit(pd2);
        Future<Boolean> result3 = service.submit(pd3);
        System.out.println(result1);
        //关闭服务
        service.shutdown();
    }
}

静态代理

  • 类似new Thread(Runnable对象).start()方法
public class StaticProxy {
	public static void main(String[] args) {
		new WeddingCompany(new You()).happyMarry();
		
		//new Thread(线程对象).start();
	}
}
interface Marry{
	void happyMarry();
}
//真实角色
class You implements Marry{

	@Override
	public void happyMarry() {
		System.out.println("you and 嫦娥终于奔月了....");
	}
	
}
//代理角色
class WeddingCompany implements Marry{
	//真实角色
	private Marry target;
	public WeddingCompany(Marry target) {
			this.target = target;
	}
	@Override
	public void happyMarry() {
		ready();
		this.target.happyMarry();
		after();
	}
	
	private void ready() {
		System.out.println("布置猪窝。。。。");
	}
	private void after() {
		System.out.println("闹玉兔。。。。");
	}
}

lambda表达式

  • java1.8开始有的功能
  • 实现函数式接口的时候可以用lambda表达式。函数式接口是指有且仅有一个抽象方法的接口(可以有其他的方法,默认,静态,私有)
  • 表达式为接口的对象 = (参数1,参数2...) -> {函数体};,这个函数就是接口定义的抽象方法
  • @FunctionalInterface注解检查接口是否为函数式接口,如果不是则会报错
  • 一些示例如下
public class TestLambda {
    public static void main(String[] args) {
        interfaceA a;
        a = () -> System.out.println("汪汪汪");
        a.shout();
        //只有一个参数的时候可以省略括号
        interfaceB b;
        b = (num -> {
            for(int i = 0;i<num;i++){
                System.out.print("汪");
            }
            System.out.println();
        });
        b.shout(10);

        //两个参数的时候
        interfaceC c ;
        c = (int x,int y) ->{
            System.out.println("汪汪"+(x+y));
        };
        c.shout(3,4);

        //有返回值
        interfaceD d;
        d = (int x,int y) ->{
            System.out.println("hh");
            return x+y;
        };
        System.out.println(d.shout(20,39));
        interfaceD.run();

    }
}
interface interfaceA{
    void shout();
}
interface interfaceB{
    void shout(int num);
}

interface interfaceC{
    void shout(int x,int y);
}

@FunctionalInterface
interface interfaceD{
    int shout(int x,int y);
    static void run(){
        System.out.println("快跑");
    }
}
  • 在多线程中使用lambda表达式,两个线程只需要用lambda的方式实现Runnable接口的void run()方法
public class TestLambdaThread {
    public static void main(String[] args) {
        /**
         * lambda表达式的多线程
         * 线程1和线程2会同时运行,每次得到的结果不一样
         * lambda实现了Runnable接口的void run()方法
         */
        //线程1
        new Thread(()->{
            for(int i = 0;i<100;i++){
                System.out.println(i);
            }
        }).start();
        //线程2
        new Thread(()->{
            for(int i = 0;i<100;i++){
                System.out.println("------"+i);
            }
        }).start();
        
    }
}

线程状态

  • 新生new,就绪start,运行,阻塞,死亡五个状态

在这里插入图片描述

  • 新生状态(New): 用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

  • 就绪状态(Runnable): 处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有4中原因会导致线程进入就绪状态:

    1. 新建线程:调用start()方法,进入就绪状态;
    
    2. 阻塞线程:阻塞解除,进入就绪状态;
    
    3. 运行线程:调用yield()方法,直接进入就绪状态;
    
    4. 运行线程:JVM将CPU资源从本线程切换到其他线程。
    
  • 运行状态(Running):在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

  • 阻塞状态(Blocked):阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有4种原因会导致阻塞:

    1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
    
    2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。
    
    3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
    
    4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。
    
  • 死亡状态(Terminated):死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。 当一个线程进入死亡状态以后,就不能再回到其它状态了

终止

  • 正常执行完毕结束
  • 外部标记,调用方法结束
public class TestStopThread implements Runnable{
    private boolean flag = true;// 标记变量,表示线程是否可中止
    private String name ;

    public TestStopThread(String name) {
        this.name = name;
    }
    public void run(){
        int i =0;
        //当flag的值是true时,继续线程体;false则结束循环,继而终止线程体
        while (flag){
            System.out.println(name+"--->"+i++);
        }
    }
    public void stop(){
        flag = false;
    }
    public static void main(String[] args) {
        TestStopThread thread = new TestStopThread("线程一");
        new Thread(thread).start();
        for(int i = 0;i<100;i++){
            System.out.println("main------------"+i);
            if (i==70){
                //i为70的时候终止线程1
                thread.stop();
                System.out.println("线程一已终止");
            }
        }
    }
}

阻塞 sleep yield

  • 暂停线程执行常用的方法有sleep()和yield()方法,这两个方法的区别是:
    1. sleep()方法:可以让正在运行的线程进入阻塞状态,休眠时间满之后,进入就绪状态。
    2. yield()方法:可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。

sleep

  • sleep,直接使用Thread.sleep(毫秒数);run方法中的sleep需要使用try catch捕获异常,不能抛出
  • sleep示例,倒计时
import java.text.SimpleDateFormat;
import java.util.Date;

public class TestBlock {
    public static void main(String[] args) throws InterruptedException {
        //倒计时的功能
        Date d = new Date(System.currentTimeMillis()+10000);
        long end = d.getTime();
        while (true){
            System.out.println(new SimpleDateFormat("hh:mm:ss").format(d));
            Thread.sleep(1000);
            d = new Date(d.getTime() - 1000);
            if(end-10000>d.getTime()){
                break;
            }
        }

    }
    static void test() throws InterruptedException {
        int num = 10;
        while (true){
            System.out.println(num--);
            Thread.sleep(1000);
            if(num == 0){
                System.out.println("点火...");
                Thread.sleep(1500);
                System.out.println("起飞");
                break;
            }
        }
    }

yield

  • 直接进入就绪状态,让CPU调度
  • 两个示例,直接使用Thread.yield();来暂停
public class TestYield {
    public static void main(String[] args) {
        //示例1
        Myclass c = new Myclass();
        new Thread(c,"1号线").start();
        new Thread(c,"2号线").start();

        //示例2 主线程礼让示例
        //新线程
        new Thread(()->{
            for(int i = 0;i<100;i++ ){
                System.out.println("lambda....."+i);
            }
        }).start();
        //主线程
        for(int i = 0;i<100;i++){
            if(i % 10 == 0){
                Thread.yield();
            }
            System.out.println("main..."+i);
        }
    }
}

class Myclass implements Runnable{
    public void run(){
        System.out.println(Thread.currentThread().getName()+"....start");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+".......end");
    }
}

插队 join

  • 线程A在运行期间,(B可能会执行),可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。联合之前,A和B可能都会执行一些。
  • join方法通过对象调用
/**
 * join示例
 * 对象.join(毫秒);
 */
public class TestJoin {
    public static void main(String[] args) throws InterruptedException {

        //join
        //新线程
        Thread t = new Thread(()->{
            for(int i = 0;i<100;i++ ){
                System.out.println("lambda....."+i);
            }
        });
        t.start();
        //主线程
        for(int i = 0;i<100;i++){
            if(i  == 10){
                //Thread.yield();
                t.join(1); //i为10的时候让t开始,t结束之后再继续
            }
            System.out.println("main..."+i);
        }
    }
}

观察线程状态

  • 五个状态 NEW,RUNNABLE,TIMED_WAITING,BLOCKED ,TERMINATED
  • 观察五个状态
public class TestThreadState {
    public static void main(String[] args) {
        Thread thread = new Thread(() ->{
            for(int i = 0;i<1;i++){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("---分线程执行结束----");
        });

        System.out.println(thread.getState()); //NEW
        thread.start();
        System.out.println(thread.getState()); //RUNNABLE
        //监控整个过程的状态
        while (true){
            int num = Thread.activeCount(); //活动的线程数量
            Thread.State state = thread.getState();
            System.out.println(state+" ----- "+ num);
            if(state.equals(Thread.State.TERMINATED)){ //如果分线程终止了,结束循环
                break;
            }
        }

    }
}

线程优先级

  • 优先级高的执行概率大
  • 处于就绪状态的线程,会进入“就绪队列”等待JVM来挑选。
  • 线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。int MIN_PRIORITY = 1,int NORM_PRIORITY = 5,int MAX_PRIORITY = 10
  • 使用下列方法获得或设置线程对象的优先级。int getPriority(); void setPriority(int newPriority);
  • 优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高的线程后调用优先级低的线程。
public class TestPriority {
    public static void main(String[] args) {
        int a = Thread.currentThread().getPriority();
        System.out.println(a);

        MyThread t = new MyThread();
        Thread t1 = new Thread(t,"线程一");
        Thread t2 = new Thread(t,"线程二");
        Thread t3 = new Thread(t,"线程三");
        Thread t4 = new Thread(t,"线程四");
        Thread t5 = new Thread(t,"线程五");
        Thread t6 = new Thread(t,"线程六");
//设置优先级
        t1.setPriority(1);
        t2.setPriority(1);
        t3.setPriority(1);
        t4.setPriority(10);
        t5.setPriority(10);
        t6.setPriority(10);
        
//启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();

    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        int prioprity = Thread.currentThread().getPriority();
        System.out.println(name+"优先级 ----> "+prioprity);
    }
}

守护线程 daemon

  • 用户线程和守护线程,守护线程为用户线程服务
  • 用户线程执行完后,不需要等守护线程结束
  • 默认是用户线程,通过Thread对象的setDaemon(true)将用户线程设置为守护线程
/**
 * 将用户线程设置为守护线程
 * 用户线程结束表示程序结束
 */
public class TestDaemon {
    public static void main(String[] args) {
        Person p = new Person();
        Earth e = new Earth();
        //将e设置为守护线程
        Thread t = new Thread(e);
        t.setDaemon(true);
        t.start();
        //p为用户线程
        new Thread(p).start();
        System.out.println(Thread.activeCount());
    }
}

class Person implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i<100;i++){
            System.out.println(i+1+"年过去了..");
        }
        System.out.println("一辈子结束了...");
    }
}
class Earth implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("地球始终在转....");
        }
    }
}

常用方法

  • isAlive是否活着,不是终止状态就是活着
  • Thread.currentThread()表示当前线程
  • setName设置线程名称,设置要在start之前
        System.out.println(Thread.currentThread().isAlive());
        Thread.currentThread().setName("hhh");
        //Thread对象的setName
        System.out.println(Thread.currentThread().getName());

线程同步

  • 保证数据准确性和安全性,同时提高准确性
  • 线程不安全的示例
import java.util.ArrayList;
import java.util.List;

public class TestUnsafe {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for(int i = 0;i<1000;i++){
            new Thread(() ->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        System.out.println(list.size());
    }
}
  • 可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和 synchronized 块。

synchronized 方法

  • 通过在方法声明中加入 synchronized关键字来声明,语法如下:public synchronized void accessVal(int newVal);
  • synchronized 方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
public class TestSynTicket implements Runnable {
    private int tickets = 30;
    public boolean flag = true;
    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }

    public synchronized void buy(){
            if(tickets==0){
                flag = false;
                System.out.println("无票");
                return;
            }
            try {
                Thread.sleep(100); //有网络延迟时,会出现负数
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 抢到一张,余票->"+ --tickets);
    }


    public static void main(String[] args) {
        TestSynTicket tk = new TestSynTicket();
        //一份资源多个线程争夺
        new Thread(tk ,"1号线").start();
        new Thread(tk,"2号线").start();
        new Thread(tk,"3号线").start();
    }
}

synchronized块

  • synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。
  • Java 为我们提供了更好的解决办法,那就是 synchronized 块。 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。
  • 语法 synchronized(syncObject) { //允许访问控制的代码   }
  • 票示例的同步块
public class TestSynTicket implements Runnable {
    private int tickets = 30;
    public boolean flag = true;
    @Override
    public void run() {
        while (flag) {
            buy5();
        }
    }
    //同步方法
    public synchronized void buy(){
            if(tickets==0){
                flag = false;
                System.out.println("无票");
                return;
            }
            try {
                Thread.sleep(100); //有网络延迟时,会出现负数
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 抢到一张,余票->"+ --tickets);
    }
    //同步块,不能锁Integer Boolean,是变化的
    //线程安全:尽可能锁定合理的范围(不是指代码 指数据的完整性)
    //double checking
    public  void buy5() {
        if(tickets<=0) {//考虑的是没有票的情况
            flag = false;
            return ;
        }
        synchronized(this) {
            if(tickets<=0) {//考虑最后的1张票
                flag = false;
                return ;
            }
            //模拟延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"-->"+tickets--);
        }
    }


    public static void main(String[] args) {
        TestSynTicket tk = new TestSynTicket();
        //一份资源多个线程争夺
        new Thread(tk ,"1号线").start();
        new Thread(tk,"2号线").start();
        new Thread(tk,"3号线").start();
    }
}


  • 容器的简单的同步示例
import java.util.ArrayList;
import java.util.List;

public class TestUnsafe {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for(int i = 0;i<100;i++){
            new Thread(() -> {
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(list.size());
    }
}

线程安全的ArrayList

  • 直接用自带线程安全的ArrayList,即 java.util.concurrent.CopyOnWriteArrayList,使用与ArrayList相同
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class TestSynList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new CopyOnWriteArrayList<>();
        for(int i = 0;i<1000;i++){
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(list.size());
    }
}

死锁

  • 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形
  • 某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题
  • 死锁示例
public class TestDeadLock {
    public static void main(String[] args) {
        Markup g1 = new Markup(1,"罗密欧");
        Markup g2 = new Markup(0,"朱丽叶");
        g1.start();
        g2.start();
    }

}
//口红
class Lipstick{

}
//镜子
class Mirror{

}
//化妆
class Markup extends Thread{
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
    //选择
    int choice;
    //名字
    String girl;
    public Markup(int choice,String girl) {
        this.choice = choice;
        this.girl = girl;
    }

    @Override
    public void run() {
        //化妆
        markup();
    }
    //相互持有对方的对象锁-->可能造成死锁
    private void markup() {
        if(choice==0) {
            synchronized(lipstick) { //获得口红的锁
                System.out.println(this.girl+"涂口红");
                //1秒后想拥有镜子的锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
				/* //如果锁放在这里就会形成死锁
				synchronized(mirror) {
					System.out.println(this.girl+"照镜子");
				}*/
            }
            //为了防止死锁需要把锁放到外面
            synchronized(mirror) {
                System.out.println(this.girl+"照镜子");
            }
        }else {
            synchronized(mirror) { //获得镜子的锁
                System.out.println(this.girl+"照镜子");
                //2秒后想拥有口红的锁
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
					/*
					synchronized(lipstick) {
						System.out.println(this.girl+"涂口红");
					}	*/
            }
            synchronized(lipstick) {
                System.out.println(this.girl+"涂口红");
            }
        }
    }
}

并发协作线程通信

管程法

  • 多线程并发协作模型“生产者/消费者模式”。
  • 生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
  • 消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
  • 消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
  • 缓冲区是实现并发的核心,缓冲区的设置有3个好处:(1) 实现线程的并发协作: 有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。(2) 解耦了生产者和消费者,生产者不需要和消费者直接打交道。(3)解决忙闲不均,提高效率,生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据
  • wait方法阻塞线程,会释放锁,使用notify唤醒,notifyAll唤醒全部
  • 以生产馒头卖馒头为例,重要的部分在缓冲区
public class TestCo1 {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Consumer(container).start();
    }
}
//生产者
class Productor extends Thread{
    SynContainer container;
    public Productor(SynContainer container){
        this.container=container;
    }

    @Override
    public void run() {
        for(int i = 0;i<100;i++){
            System.out.println("生产-->"+i);
            container.push(new Steambun(i));
        }
    }
}
//消费者
class Consumer extends Thread{
    SynContainer container;
    public Consumer(SynContainer container){
        this.container=container;
    }
    @Override
    public void run() {
        for(int i=0;i<100;i++){
            System.out.println("消费-->"+container.pop().id);
        }
    }
}
//缓冲区
class SynContainer{
    Steambun[] buns = new Steambun[10];
    int count = 0;
    //存储
    public synchronized void push(Steambun bun){
        //生产时间,容器存在空间可以生产
        if(count == buns.length){
            //不能生产
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        buns[count]=bun;
        count++;
        //存在数据,可以通知消费
        this.notifyAll();
    }
    //获取消费
    public  synchronized Steambun pop(){
        //消费的时机,判断容器中是否有数据
        //没有数据就等待,调用wait方法
        if(count == 0){
            try {
                this.wait(); //阻塞线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //存在数据就可以消费
        count--;
        Steambun bun = buns[count];
        this.notifyAll(); //存在空间,唤醒生产
        return bun;
    }
}
//馒头
class Steambun{
    int id;

    public Steambun(int id) {
        this.id = id;
    }
}

信号灯法

  • 示例
public class TestCoRedLight {
    public static void main(String[] args) {
        Tv tv = new Tv();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

//生产者 演员
class Player extends Thread{
    Tv tv;
    public Player(Tv tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for(int i=0;i<20;i++){
            if(i%2==0){
                tv.play("小品");
            }else{
                tv.play("相声");
            }
        }
    }
}
//消费者 观众
class Watcher extends Thread{
    Tv tv;
    public Watcher(Tv tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for(int i=0;i<20;i++){
            tv.watch();
        }
    }
}
//同一个资源 电视
class Tv{
    String voice;
    //true演员表演,观众等待,false观众观看,演员等待
    boolean flag = true;
    //表演
    public synchronized void play(String voice){
        if(!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //表演
        System.out.println("表演了.."+voice);
        this.voice=voice;
        this.notifyAll();
        this.flag=!flag;
    }
    //观看
    public synchronized void watch(){
        if(flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("听到了.."+voice);
        this.notifyAll();
        this.flag=!flag;
    }
}
/*
表演了..小品
听到了..小品
表演了..相声
听到了..相声
表演了..小品
听到了..小品
*/

高级操作

任务定时调度

  • 使用java.util.Timer和TimerTask。TimerTask指定任务,Timer对任务进行时间调度
  • 延迟执行任务,指定时间执行任务,每几秒运行一次,
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 任务定时调度,借助Timer和TimerTask
 */
public class TestTimer {
    public static void main(String[] args) {
        Timer timer = new Timer();
        //延迟一秒,执行一次进入等待
        //timer.schedule(new MyTask(),1000);

        //延迟5秒开始执行,以后每3秒执行一次
        timer.schedule(new MyTask(),5000,3000);

        //指定的时间执行,注意1月从0开始
        //Calendar calendar = new GregorianCalendar(2020,1,22,9,30,0);
        //timer.schedule(new MyTask(),calendar.getTime(),3000);

        //2020-02-22 09:30:00
        //2020-02-22 09:30:03
        //2020-02-22 09:30:06
        //2020-02-22 09:30:09

        //System.out.println(calendar.getTime());

    }
}
//任务类,run方法中写要执行的任务
class MyTask extends TimerTask{
    @Override
    public void run() {
        for(int i = 0;i<1;i++){
            //System.out.println("哈哈哈哈");
            String t = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(System.currentTimeMillis());
            System.out.println(t);
        }
    }
}

Quartz

//SimpleExample.java
import static org.quartz.DateBuilder.evenMinuteDate;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;

import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;

public class SimpleExample {

  public void run() throws Exception {
    Logger log = LoggerFactory.getLogger(SimpleExample.class);

    log.info("------- Initializing ----------------------");

    // First we must get a reference to a scheduler
    SchedulerFactory sf = new StdSchedulerFactory();
    Scheduler sched = sf.getScheduler();

    log.info("------- Initialization Complete -----------");

    // computer a time that is on the next round minute
    //下一分钟开始执行
    Date runTime = evenMinuteDate(new Date());

    log.info("------- Scheduling Job  -------------------");

    // define the job and tie it to our HelloJob class
    JobDetail job = newJob(HelloJob.class).withIdentity("job1", "group1").build();

    // Trigger the job to run on the next round minute
    //每隔五秒执行一次,一共执行3次
    Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime).
            withSchedule(simpleSchedule().withIntervalInSeconds(5).withRepeatCount(3)).build();

    // Tell quartz to schedule the job using our trigger
    sched.scheduleJob(job, trigger);
    log.info(job.getKey() + " will run at: " + runTime);

    // Start up the scheduler (nothing can actually run until the
    // scheduler has been started)
    sched.start();

    log.info("------- Started Scheduler -----------------");

    // wait long enough so that the scheduler as an opportunity to
    // run the job!
    log.info("------- Waiting 65 seconds... -------------");
    try {
      // wait 65 seconds to show job
      Thread.sleep(65L * 1000L);
      // executing...
    } catch (Exception e) {
      //
    }

    // shut down the scheduler
    log.info("------- Shutting Down ---------------------");
    sched.shutdown(true);
    log.info("------- Shutdown Complete -----------------");
  }

  public static void main(String[] args) throws Exception {

    SimpleExample example = new SimpleExample();
    example.run();

  }
}
  • 运行的任务
//HelloJob.java
import java.text.SimpleDateFormat;
import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class HelloJob implements Job {

    private static Logger _log = LoggerFactory.getLogger(HelloJob.class);

    public HelloJob() {
    }

    public void execute(JobExecutionContext context)
        throws JobExecutionException {
        System.out.println("哈哈哈哈哈哈----"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(System.currentTimeMillis()));
        _log.info("Hello World! - " + new Date());
    }

}

happenbefore

  • cpu根据重排指令对代码执行的顺序重排
  • 例如下面的代码,当a==0的时候可能会输出a的值是1
public class TestHappenBefore {
    private static int a=0;
    private static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
//        for(int i=0;i<100;i++){
//            a=0;
//            flag=false;
            Thread t1 = new Thread(()->{
                System.out.println("Thread1");
                a=1;
                flag=true;
            });
            Thread t2 = new Thread(()->{
                System.out.println("Thread2");
                if(flag){
                    a*=1;
                }
                if(a==0){
                    System.out.println("happen before a = "+a);
                }
            });
            t2.start();
            t1.start();
            t1.join();
            t2.join();
        //}
    }
}

volatile

  • 保证线程间变量的可见性。线程对变量进行修改之后要立刻写会主内存,线程读取变量的时候从主内存读取,而不是缓存
  • 保证数据的同步
public class TestVolatile {
    //本例中,如果不加volatile修饰,就会死循环,加了后保证了数据的同步
    private volatile static int num=0;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
           while (num==0){
            //死循环
           }
        }).start();
        Thread.sleep(1000);//1秒钟以后修改变量,while循环却没有终止
        num=1;
    }
}

单例模式

  • 保证在多线程环境下,对外存在一个对象
  • 1、构造器私有化 -->避免外部new构造器
  • 2、提供私有的静态属性 -->存储对象的地址
  • 3、提供公共的静态方法 --> 获取属性
public class SingleObj {
    //私有静态属性
    private static volatile SingleObj instance;
    //构造器私有化
    private SingleObj(){
    }
    //提供公共的静态方法
    public static SingleObj getInstance(){
        //减少不必要的同步
        if(instance != null){
            return instance;
        }
        //同步对象,防止重复创建对象,由于是静态方法,不能锁定this
        synchronized (SingleObj.class){
            //对象为空时创建对象
            if(instance == null){
                instance = new SingleObj();
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println(SingleObj.getInstance());
        });
        t.start();
        System.out.println(SingleObj.getInstance());
    }
}

ThreadLocal

  • 线程自己的局部变量存储空间
  • 多线程下保证成员变量的安全
  • 使用private static修饰,常用方法get set initialValue
  • 每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值
/**
 * 每个线程都有自己的变量,不同线程间不影响
 * 构造器被谁调用就是谁的线程
 * InheritableThreadLocal能够获得调用者的threadlocal的值的副本
 */
public class TestThreadLocal {
    //不指定初始值,默认为null
    //private static ThreadLocal<Integer> t = new ThreadLocal<>();
    //指定初始值
    private static ThreadLocal<Integer> t1 = ThreadLocal.withInitial(()->{
        return 200;
    });
    public static class MyRun implements Runnable{
        public MyRun(){
            t1.set(7); //修改的是main线程的
            System.out.println(Thread.currentThread().getName()+"------"+t1.get());
        }
        @Override
        public void run() {
            //run中是开启的新线程
            t1.set((int)(Math.random()*99)); //修改的是新线程
            System.out.println(Thread.currentThread().getName()+"---"+t1.get());
        }
    }

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"---"+t1.get());//main线程的值
        t1.set(10000); //设置值
        System.out.println(Thread.currentThread().getName()+"---"+t1.get());//main线程的值
        new Thread(new MyRun()).start();//线程1的值,MyRun构造的时候是main线程,调用run之后是新线程
        new Thread(new MyRun()).start();//线程2的值
    }
}

可重入锁

  • 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞,不可重入锁则会阻塞。
  • 示例
public class TestLock1 {
    public void test(){
        synchronized (this){
            while (true){
                synchronized (this){
                    System.out.println("Reintain lock");
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        new TestLock1().test();
    }
}
  • 不可重入锁
/**
 * 不可重入锁示例
 */
public class TestLock2 {
    Lock lock = new Lock();
    public void a() throws InterruptedException {
        lock.lock();
        doThing();
        lock.unlock();
    }
    //不可重入锁
    public void doThing() throws InterruptedException {
        lock.lock();
        lock.unlock();
    }

    public static void main(String[] args) throws InterruptedException {
        TestLock2 t = new TestLock2();
        t.a();
        t.doThing();
    }
}
class Lock{
    private boolean isLocked = false;
    //使用
    public synchronized void lock() throws InterruptedException {
        while (isLocked){
            wait();
        }
        isLocked = true;
    }
    //释放
    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}
  • 可重入锁,如果锁是当前线程的,继续使用当前锁,锁计数器加1,如果当前线程释放锁,锁的数量减1,但还是没有释放锁,当锁的数量为0的时候,就可以释放锁,其他的线程可以使用了
  • 可重入锁的简单实现示例
/**
 * 可重入锁示例
 */
public class TestLock3 {
    ReLock lock = new ReLock();
    public void a() throws InterruptedException {
        lock.lock();
        System.out.println("锁数量A "+lock.getHoldCount());
        System.out.println("start");
        doThing();
        lock.unlock();
        System.out.println("锁数量B "+lock.getHoldCount());
    }
    //不可重入锁
    public void doThing() throws InterruptedException {
        lock.lock();
        System.out.println("锁数量C "+lock.getHoldCount());
        System.out.println("hh");
        lock.unlock();
        System.out.println("锁数量D "+lock.getHoldCount());
    }

    public static void main(String[] args) throws InterruptedException {
        TestLock3 t = new TestLock3();
        t.a();
        //t.doThing();
    }
}

class ReLock{
    //可重入锁
    //是否占用
    private boolean isLocked = false;
    private Thread lockedBy = null; //哪个线程占用
    private int holdCount = 0; //锁的数量
    //使用
    public synchronized void lock() throws InterruptedException {
        Thread current = Thread.currentThread();
        //如果锁被占用并且是其他线程,那么等待
        while (isLocked && lockedBy != current){
            wait();
        }
        isLocked = true;
        lockedBy = current;
        holdCount ++;
    }
    //释放
    public synchronized void unlock(){
        //当前线程的时候释放
        if(Thread.currentThread() == lockedBy){
            holdCount--; //锁数量减去1
            //数量为0的时候就释放
            if(holdCount==0){
                isLocked = false;
                notify();
                lockedBy = null;
            }
        }
    }
    //锁数量的getter方法
    public int getHoldCount() {
        return holdCount;
    }
}
  • 自带的可重入锁使用,lock和unlock方法,getHoldCount获取锁的数量等
public class TestLock4 {
    ReentrantLock lock = new ReentrantLock();
    public void a() throws InterruptedException {
        lock.lock();
        System.out.println("锁数量A "+lock.getHoldCount());
        System.out.println("start");
        doThing();
        lock.unlock();
        System.out.println("锁数量B "+lock.getHoldCount());
    }
    //不可重入锁
    public void doThing() throws InterruptedException {
        lock.lock();
        System.out.println("锁数量C "+lock.getHoldCount());
        System.out.println("hh");
        lock.unlock();
        System.out.println("锁数量D "+lock.getHoldCount());
    }

    public static void main(String[] args) throws InterruptedException {
        TestLock4 t = new TestLock4();
        t.a();
        //t.doThing();
    }
}

CAS乐观锁

  • 乐观锁。每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。乐观锁适合多读的场景,实现思想有version方式和CAS方式。
  • 悲观锁 :每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。在Java中,synchronized的思想也是悲观锁。
  • java.util.concurrent.atomic.* 其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入
import java.util.concurrent.atomic.AtomicInteger;

public class TestCAS {
    private static AtomicInteger num = new AtomicInteger(5);
    public static void main(String[] args) {
        for(int i = 0;i<10;i++){
            new Thread(()->{
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Integer left = num.decrementAndGet();
                if(left<1){
                    System.out.println(Thread.currentThread().getName()+"-----卖完了");
                    return;
                }

                System.out.println(Thread.currentThread().getName()+"抢到了一个,还剩:"+left);
            }).start();
        }
    }
}

附 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.king</groupId>
    <artifactId>testjava</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->

    <dependencies>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
         </dependency>
        <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.30</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

独孤尚亮dugushangliang

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

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

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

打赏作者

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

抵扣说明:

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

余额充值