JavaSE进阶回顾第六天-多线程

第六章

  • 多线程概述
  • 实现线程有两种方式
  • 线程对象的生命周期
  • 线程的常用方法
  • 线程的安全synchronized(重点)
  • 死锁
  • 线程这块的其他内容
多线程概述
  1. 什么是进程?什么是线程?、

​ 进程是一个应用程序(1个进程是一个软件)。

线程是一个进程中的执行场景/执行单元。

​ 一个进程可以启动多个线程

  1. 对于java程序来说,当在DOS命令窗口中输入:

​ java Helloworld 回车之后

​ 会先启动JVM,而JVM就是一个进程。

​ JVM会再启动一个主线程,调用main方法。

​ 同时会再启动一个垃圾回收线程负责看护,回收垃圾。

​ 最起码,现在的java程序中有两个线程并发。

  1. 进程与进程和线程与线程是是什么关系?
    在这里插入图片描述

进程A和进程B的内存独立不共享。(魔兽游戏与酷狗音乐)

线程A和线程B内存,堆内存和方法区内存共享。栈内存独立,一个线程一个栈。

假设启动10个线程,会有10个栈空间,栈与栈之间互不干扰,各自执行各自的,这就是多线程并发。

  1. 思考一个问题:

    使用了多线程机制之后,main方法结束,是不是有可能出现也不会结束。

    main方法结束只是主线程结束了主栈空了,其他的栈(线程)可能还在压栈弹栈。

  2. 分析一个问题:对于单核的cpu来说,真的可以做到真正的多线程并发吗?

​ 什么是真正的多线程并发?

​ t1线程执行t1的

​ t2线程执行t2的

​ t1不会影响t2,t2也不会影响t1。这叫做真正的多线程并发。

​ 单核的cpu表示只有一个大脑。

  1. java语言中,实现线程有两种方式,哪两种方式呢?

​ 方式一:编写一个类,直接继承java.lang.Thread,重写run方法。

​ 方式二:编写一个类实现java.lang.Runnable接口。(常用!!!)

实现线程有两种方式
/**
 * 大家分析一下程序,有几个线程,除垃圾回收线程之外,有几个线程?
 *      一个线程(因为程序只有一个栈)
 *
 * main begin
 * m1 begin
 * m2 begin
 * m3 execute!
 * m2 over
 * m1 over
 * main over
 *      一个栈中自上而下的顺序依次逐行执行。
 */
public class ThreadTest01 {
    public static void main(String[] args) {
        System.out.println("main begin");
        m1();
        System.out.println("main over");
    }

    private static void m1() {
        System.out.println("m1 begin");
        m2();
        System.out.println("m1 over");
    }

    private static void m2() {
        System.out.println("m2 begin");
        m3();
        System.out.println("m2 over");
    }

    private static void m3() {
        System.out.println("m3 execute!");
    }
}

方式一:编写一个类,直接继承java.lang.Thread,重写run方法。

/**
 * 实现线程的第一种方式:
 *      编写一个类,直接继承java.lang.Thread,重写run方法。
 *
 *      怎么创建线程对象? new就行了。
 *      怎么启动线程? 调用线程对象的start()方法。
 *
 * 注意:
 *      亘古不变的道理:
 *          方法体当中的代码永远都是自上而下的顺序。
 */
public class ThreadTest02 {
    public static void main(String[] args) {
        //这里是main方法,这里的代码属于主线程,在主栈中运行。
        //新建一个分支线程对象。
        MyThread myThread = new MyThread();
        //启动线程
        //myThread.run();//直接调run不会启动线程,不会分配新的分支栈。(这种方式就是单线程的)
        //start()方法的作用是:启动一个分支线程,在JVM中开辟一个栈空间,这段代码任务完成之后,瞬间就结束了。
        //启动成功的线程会自动调用run方法,并且run方法在分支栈的底部(压栈)。run和main是平级的,实现了两个线程并发。
        myThread.start();
        //这里的代码还是运行在主线程中。
        for (int i=0;i<1000;i++){
            System.out.println("主线程->"+i);
        }
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        //编写程序,这段程序运行在分支线程中(分支栈)。
        for(int i=0;i<1000;i++){
            System.out.println("线程分支->"+i);
        }
    }
}
/*
运行结果:通过调用start()方法,明显看得出main线程和分支线程是并发执行的。
...
主线程->7
主线程->8
线程分支->0
线程分支->1
...
*/

直接调run的内存图:

在这里插入图片描述

调用start的内存图:
在这里插入图片描述

方式二:编写一个类实现java.lang.Runnable接口。(常用!!!)

/**
 * 实现线程的第二种方式,编写一个类实现java.lang.Runnable接口。
 *
 */
public class ThreadTest03 {
    public static void main(String[] args) {
        //创建一个可运行的对象
        //MyRunnable r = new MyRunnable();
        //将可运行的对象封装成一个线程对象。
        //Thread t = new Thread(r);
        //或者直接一步
        Thread t = new Thread(new MyRunnable());

        //启动线程
        t.start();

        for (int i=0;i<100;i++){
            System.out.println("主线程-->"+i);
        }
    }
}

//这并不是一个线程类,是一个可运行的类。它还不是一个线程。
class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i=0;i<100;i++){
            System.out.println("分支线程-->"+i);
        }
    }
}
/*
运行结果:
...
主线程-->7
主线程-->8
分支线程-->0
分支线程-->1
主线程-->9
...
*/
/**
 * 采用匿名内部类可以吗? 可以!
 *
 */
public class ThreadTest04 {
    public static void main(String[] args) {
        Thread t =new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i =0;i<100;i++){
                    System.out.println("分支线程-->"+i);
                }
            }
        });

        //启动分支线程
        t.start();

        for (int i=0;i<100;i++){
            System.out.println("主线程-->"+i);
        }
    }
}
/*
运行结果:
...
主线程-->4
主线程-->5
分支线程-->0
分支线程-->1
...
*/

注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他类,更灵活。

关于线程对象的生命周期?(面试会问!!)

在这里插入图片描述

关于线程对象的生命周期?

新建状态

就绪状态

运行状态

阻塞状态

死亡状态

线程的常用方法
/**
 * 1、怎样获取当前线程对象?
 *    Thread t = Thread.currentThread();
 *    返回值t就是当前线程。
 * 2、获取线程对象的名字
 *    String name = 线程对象.getName();
 * 3、修改线程对象的名字
 *    线程对象.setName("线程名字");
 *
 * 4、当线程没有设置名字,默认的有什么规律?
 * Thread-0
 * Thread-1
 * Thread-2
 * Thread-3
 *  ......
 */
public class ThreadTest05 {
    public static void main(String[] args) {

        //获取当前线程对象
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName());//main
        //创建线程对象
        MyThread2 t = new MyThread2();
        //设置线程的名字
        //t.setName("tttt");
        //获取线程的名字
        System.out.println(t.getName());//Thread-0

        //启动线程
        t.start();

    }
}

class MyThread2 extends Thread{
    @Override
    public void run() {
        for (int i=0;i<100;i++){
            Thread currentThread = Thread.currentThread();
            //哪个线程启动它,哪个线程就是当前线程。t.start(); t就是当前对象
            System.out.println(currentThread.getName()+"--->"+i);
        }
    }
}

线程的sleep方法
/**
 * 关于线程的sleep方法:
 *    static void sleep(long millis);
 *    1、静态方法
 *    2、参数是毫秒
 *    3、作用:让当前线程进入休眠,进入“阻塞”状态,放弃占用cpu片段,让给其他线程使用。
 */
public class ThreadTest06 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000*5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("分支线程");//5秒之后执行
            }
        });

        //启动线程
        t.start();
        System.out.println("hello world");
        System.out.println("============================");

        for (int i=0;i<10;i++){
            //睡眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
    }
}
/*
运行结果:
hello world
============================
main-->0
main-->1
main-->2
main-->3
分支线程
main-->4
main-->5
main-->6
main-->7
main-->8
main-->9
*/
sleep相关面试题
/**
 * Thread.sleep()方法的面试题
 */
public class ThreadTest07 {
    public static void main(String[] args) {
        //创建线程对象
        MyThread3 t = new MyThread3();
        t.setName("t");
        //启动
        t.start();

        //调用sleep方法
        try {
            //问题:这行代码会让线程t进入休眠状态码? 不会,因为sleep是静态方法
            t.sleep(1000*5);//在执行的时候还是会转换成:Thread.sleep(1000*5);
                            //这行代码作用是让当前线程进入休眠,也就是main线程。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main over");//5秒之后输出
    }
}

class MyThread3 extends Thread{
    @Override
    public void run() {
        for (int i=0;i<10000;i++){
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}
终止sleep方法
/**
 * sleep睡太久了,如果希望半道醒来,你应该怎么办?也就是怎么叫醒正在睡眠的线程。
 * 注意:这个不是终止线程的执行,是终止线程的睡眠。
 */
public class ThreadTest08 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable2());
        t.setName("t");
        t.start();//启动

        //希望5秒之后,t线程醒来。
        try {
            Thread.sleep(1000*5);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //终断t线程的睡眠(这种终断的方式依靠了java的异常处理机制,使其直接进入catch语句)
        t.interrupt();//


        System.out.println("main over");
    }
}
class MyRunnable2 implements Runnable{
    //重点:为什么这里不能throws异常,只能try..catch?因为子类重写父类方法,子类不能比父类抛出更多的异常。
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"--->begin");
        try {
            Thread.sleep(1000*365*24*60*60);//睡眠1年
        } catch (InterruptedException e) {
            //打印异常信息
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"--->end");
        
    }
}
强行终止线程
/**
 * 怎样合理的终止一个线程的执行。这种方式是很常用的。
 */
public class ThreadTest10 {
    public static void main(String[] args) {
        MyRunnable4 r = new MyRunnable4();
        Thread t = new Thread(r);
        t.setName("t");
        t.start();

        //模拟5秒
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //终止线程
        r.run=false;

    }
}
class MyRunnable4 implements Runnable{

    //打一个布尔标记
    boolean run = true;
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            if (run){
                System.out.println(Thread.currentThread().getName()+"--->"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } 
            }else {
                //return就结束了
                //这里可以保存 save...
                //终止当前线程
                return;
            }
        }
    }
}
线程调度的方法

java中提供哪些方法是和线程调度有关系的呢?

实例方法:

实例方法:
void setPriority(); //设置线程的优先级
int getPriority(int newPriority); //获取线程优先级
/*
最低优先级1
默认优先级5
最高优先级10
优先级比较高的获取CPU时间片可能会多一些。
*/

静态方法:
static void yield(); //让位方法
/*
暂停当前正在执行的线程对象,并执行其他线程。
yield()方法不是阻塞方法,让当前线程让位,让给其他线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意:在回到就绪状态之后,有可能还会再次抢到。

实例方法:
void join()
合并线程
class MyThread1 extends Thread{
	public void doSome(){
	MyThread2 t = new MyThread2();
	MyThread2 t2 = new MyThread2();
	t.join();//当前线程进入阻塞,t线程执行,直到t线程结束,当前线程才可以继续。 
}
}

class MyThread2 extends Thread{
	
}
*/

线程的优先级:

/**
 * 了解:关于线程的优先级
 */
public class ThreadTest11 {
    public static void main(String[] args) {
        //设置主线程优先级为1
        Thread.currentThread().setPriority(1);
        /*System.out.println("最高优先级:"+Thread.MAX_PRIORITY);//10
        System.out.println("最低优先级:"+Thread.MIN_PRIORITY);//1
        System.out.println("默认优先级:"+Thread.NORM_PRIORITY);//5*/

        //获取当前线程对象的优先级
        //System.out.println(Thread.currentThread().getPriority());//5

        Thread t = new Thread(new MyRunnable5());
        t.setPriority(10);
        t.setName("t");
        t.start();

        //优先级高的,只是抢到的CPU的时间片相对多一些。
        //大概率是偏向优先级高的。
        for (int i=0;i<1000;i++){
            System.out.println("主线程--->"+i);
        }
    }
}

class MyRunnable5 implements Runnable{

    @Override
    public void run() {
        //获取线程优先级
        System.out.println(Thread.currentThread().getName()+"线程的默认优先级:"+Thread.currentThread().getPriority());//5
        for (int i=0;i<1000;i++){
            System.out.println("分支线程--->"+i);
        }
    }
}
线程让步(yield)
/**
 * 让位,当线程暂停,回到就绪状态,让给其他线程。
 * 静态方法:Thread.yield();
 *
 */
public class ThreadTest12 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable6());
        t.setName("t");
        t.start();

        //main线程让步
        Thread.yield();


        for (int i=0;i<10;i++){
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+"--->"+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyRunnable6 implements Runnable{

    @Override
    public void run() {
        for (int i =0;i<10;i++){
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
线程的合并/阻塞方法(join)
/**
 * join();
 * 合并/阻塞 线程的方法
 */
public class ThreadTest13 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new MyRunnable7());
        t.setName("t");
        t.start();

        //线程合并/阻塞 当前线程
        t.join();//t合并到当前线程中,当前线程受阻塞,t线程执行直到结束,

        for (int i=0;i<10;i++){
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyRunnable7 implements Runnable{

    @Override
    public void run() {
        for (int i=0;i<10;i++){
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
线程的安全(重点)

什么时候数据在多线程并发的情况会出现异常?

三个条件:

​ 条件1:多线程并发。

​ 条件2:有共享的数据。

​ 条件3:共享数据有修改的行为。

满足以上3个条件之后,就会存在线程安全问题。
在这里插入图片描述

怎么解决线程安全问题?

线程排队执行(不能并发)。

​ 这种机制被称为:线程同步机制。

​ 线程同步就是线程排队了,线程排队了就会牺牲一部分效率。(先考虑安全,在考虑效率!!)

异步编程模型:

​ 线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1。

​ 谁也不需要等谁,其实就是多线程并发(效率较高)。

同步编程模型:

​ 线程t1和线程t2,在线程t1执行的时候,必须等待t2线程结束,

或者说在t2线程执行的时候,必须等待t1线程结束。两个线程之间发生了等待关系,

这就是同步编程模型,效率较低,线程排队执行。

记住:异步就是并发,同步就是排队!

模拟银行取款(编写两个线程)重要!!!

多线程不安全的情况:

账户类:

/*
银行账户
    不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题。
 */
public class Account {
    //账户
    private String actno;
    //余额
    private double balance;

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    @Override
    public String toString() {
        return "Account{" +
                "actno='" + actno + '\'' +
                ", balance=" + balance +
                '}';
    }

    public void withdraw(double money){
        //取款之前
        double before=this.getBalance();
        //取款之后
        double after=before-money;

        //在这里模拟一下网络延迟,100%会出问题
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //更新余额
        //思考:t1执行到这了,但是还没来得及执行改行修改代码,t2线程进来了该方法
        //此时一定出问题
        this.setBalance(after);
    }
}

线程类:

public class AccountThread extends Thread{
    //两个线程必须共享同一个账户对象
    private Account account;

    //通过构造方法传过来
    public AccountThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        //t1和t2并发这个方法。。。(t1和t2是两个栈,两个栈操作堆中同一个对象。)
        //run方法执行表示取款操作。
        //假设取款5000
        double money = 5000;
        //取款
        //多线程并发执行这个方法
        account.withdraw(money);
        System.out.println("线程"+Thread.currentThread().getName()+"取款成功,剩余"+account.getBalance()+"元");
    }
}

测试类:

/**
 * 模拟银行取款(编写两个线程)
 */
public class Test {
    public static void main(String[] args) {
        //创建账户
        Account account = new Account("wl",10000);
        //创建两个线程对象
        AccountThread t1 = new AccountThread(account);
        AccountThread t2 = new AccountThread(account);
        t1.setName("t1");
        t2.setName("t2");
        //启动线程取款
        t1.start();
        t2.start();

        System.out.println(account.getBalance());//这里输出是10000,因为start执行是一瞬间。
    }
}
/*
运行结果:
10000.0
线程t1取款成功,剩余5000.0元
线程t2取款成功,剩余5000.0元
*/

改进后的线程安全的情况:

账户类:

/**
 * 账户类
 */
public class Account {
    //账户
    private String actno;
    //余额
    private double balance;

    private Object obj = new Object();

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    @Override
    public String toString() {
        return "Account{" +
                "actno='" + actno + '\'' +
                ", balance=" + balance +
                '}';
    }

    public void withdraw(double money){//取款
        //以下这几行代码必须是线程排队,不能并发。
        //一个线程把这里的代码全部指向结束之后,另一个线程才能进来。
        /*
        线程同步/排队机制的语法是:
        synchronized (){
            //线程同步代码块。
        }
        synchronized后面小括号传的这个"数据"是相当关键的。
        这个数据必须是多线程共享的数据。才能达到多线程排队。

        ()中写什么?
            那要看你想让那些线程同步。
            假设t1、t2、t3、t4、t5有5个线程
            你只希望t1、t2、t3排队,t4、t5不要排队,怎么办?
            你一定要在()中写一个t1、t2、 t3共享的对象。而这个对象
            对于t4、t5来说不是共享的。

            这里共享的是:账户对象
            账户对象是共享的,namethis就是账户对象!!!
            不一定是this,只要是共享的那个对象就行了。

            在java语言中,任何一个对象都有“一把锁”,其实锁就是一个标记。
            100个对象100把锁。1个对象1把锁。
                1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
                2、假设t1先执行了,遇到synchronized,这个时候自动找共享对象的对象锁,
                找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中
                一直都占有这把锁,直到同步代码块执行结束后,这把锁才会释放。
                3、假设t1已经占有了这把锁,此时t2也遇到synchronized关键字,也会去
                占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面
                等待t1的结束,直到t1把同步代码块结束了。此时t2终于等到这把锁,然后t2
                占有这把锁之后,进行同步代码块的执行。这样就实现了同步排队执行。
         */
        Object obj2 = new Object();
        //synchronized (obj) 可以
        //synchronized (obj2) 不可以,因为每个线程的obj2都不是一个共享数据对象!!
        //synchronized ("abc") 可以 因为在字符串常量池,只有一个。 但是所有线程都会同步。
        //synchronized (null) 不可以 会报错 空指针异常。
        synchronized (this){
            //取款前
            double before=this.getBalance();
            //取款后
            double after=before-money;

            //这里存在线程安全隐患问题!!!

            //取款
            this.setBalance(after);
        }

在这里插入图片描述

注意:这里的锁池不是一种状态,可以理解为一种堵塞之一。

线程类:

public class AccountThread extends Thread{
    private Account account;

    public AccountThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {//线程调用account对象的取款方法
        double money = 5000;
        account.withdraw(money);//取款5000
        System.out.println("线程"+Thread.currentThread().getName()+"取款成功,剩余"+account.getBalance()+"元");
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        Account account = new Account("王磊",10000);

        AccountThread t1 = new AccountThread(account);
        AccountThread t2 = new AccountThread(account);
        t1.setName("t1");
        t2.setName("t2");

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

        System.out.println(" "+account.getBalance());

    }
}
/*
运行结果:
线程t2取款成功,剩余0.0元
线程t1取款成功,剩余0.0元
 10000.0
 或
 线程t2取款成功,剩余0.0元
 10000.0
 线程t1取款成功,剩余5000.0元
 ...(线程安全的)
*/

synchronized(){}中()传this和"abc"的区别:

​ 传"abc"的话,所有的线程都要排队。

​ 传this的话,共享同一个this当前对象的线程需要排队,不共享的不用排队。如以下代码,t1和t2共享account

需要排队,而t3和t1、t2不共享同一个对象,因此t3不需要排队。

public class Test {
    public static void main(String[] args) {
        Account account = new Account("王磊",10000);

        AccountThread t1 = new AccountThread(account);
        AccountThread t2 = new AccountThread(account);
        
        Account account2 = new Account("李四",10000);
        AccountThread t3 = new AccountThread(account2);

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

        System.out.println(" "+account.getBalance());

    }
}
哪些变量存在线程安全问题?

1、局部变量永远都不会存在线程安全问题,因为局部变量不共享。(一个线程一个栈。),栈中的元素不共享。

2、实例变量在堆中,堆只有1个。

3、静态变量在方法区中,方法区只有1个

4、常量没有线程安全问题,因为常量不可修改。

堆和方法区都是多线程共享的,所有可能存在多线程安全问题。

synchronized(account){

account.withdraw();

}

以上写法扩大synchronized同步代码块的范围是可以的,但是效率更低了。

把account换成this可以吗? 不可以

​ 因为这里的this是AccountThread对象,这个对象不共享!该线程对象new了两次。

在实例方法上出现synchronized关键字

账户类:

public synchronized void withdraw(double money){
        //取款之前
        double before=this.getBalance();
        //取款之后
        double after=before-money;

        //在这里模拟一下网络延迟,100%会出问题
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //更新余额
        //思考:t1执行到这了,但是还没来得及执行改行修改代码,t2线程进来了该方法
        //此时一定出问题
        this.setBalance(after);
    }

分析这种方式:

​ synchronized出现在实例方法上,一定锁的是this。没得挑。只能是this。不能是其他对象了。

​ 另外,还有一个缺点:synchronize出现在实例方法上,表示整个方法体都需要同步,可能会

无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。

​ 优点:代码写得少了。

如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。

如果要使用局部变量,是选择StingBuffer还是StringBuilder?

​ StringBuilder,因为局部变量没有线程安全问题,同时StringBuilder的方法没有synchronized修饰,

执行效率更高。

ArrayList是非线程安全的。

Vector是线程安全的。

HashMap、HashSet是非线程安全的。

Hashtable是线程安全的。

总结synchronized用法:

第一种:同步代码块

​ 灵活

​ synchronized(线程共享对象){

​ 同步代码块;

}

第二种:在实例方法上使用synchronized

​ 表示共享对象一定是this。

​ 并且同步代码块是整个方法体。

第三种:在静态方法上使用synchronized

​ 表示找类锁。

​ 类锁永远只有1把。

​ 就算创建了100个对象,类锁也只有1把。

注意:

对象锁:1个对象1把锁,100个对象100把锁。

类锁:100个对象,也可能只是1把锁

对象锁用来保护实例变量的安全。

类锁用来保护静态变量的安全。

synchronized面试题

练习1:以下代码中,线程t2会不会等待t1执行完再执行?

/**
 * 面试题1
 */
public class exam01 {
    public static void main(String[] args) {
        MyClass c = new MyClass();
        MyThread t1 = new MyThread(c);
        MyThread t2 = new MyThread(c);
        t1.setName("t1");
        t2.setName("t2");

        //启动
        t1.start();
        try {
            Thread.sleep(1000);//保证t1先执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();

    }
}

class MyThread extends Thread{
    private MyClass c;

    public MyThread(MyClass c) {
        this.c = c;
    }

    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){
            c.doSome();
        }else {
            c.doOther();
        }
    }
}

class MyClass{
    public synchronized void doSome(){
        System.out.println("doSome begin!");
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over!");
    }

    public void doOther(){
        System.out.println("doOther begin!");
        System.out.println("doOther over!");
    }
}
/*
运行结果:
doSome begin!
doOther begin!
doOther over!
doSome over!
*/

答:不会,因为doOther方法没有被synchronized修饰。

练习2:

将练习1的doOther方法用synchronized关键字修饰,线程t2会不会等待t1执行完再执行。 会。

练习3:

将练习2的Test方法中,new 两个MyClass对象,分别传给两个线程,线程t2会不会等待t1执行完再执行。 不会。

因为两个对象两把锁。

练习4:将练习3的doSome方法改成静态static方法,类锁。线程t2会不会等待t1执行完再执行? 会。

因为静态方法是类锁,不管创建几个对象,类锁只有1把。

排他锁 synchronized

互斥锁

死锁(重点)

在这里插入图片描述

/**
 *死锁代码要会写(面试要写)。
 * 因为死锁很难调试。
 */
public class DeadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        MyThread1 t1 = new MyThread1(o1, o2);
        MyThread1 t2 = new MyThread1(o1, o2);
        t1.setName("t1");
        t2.setName("t2");

        //启动
        t1.start();
        t2.start();
    }
}

class MyThread1 extends Thread{
    private Object o1;
    private Object o2;

    public MyThread1(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        synchronized (o1){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){

            }
        }
    }
}

class MyThread2 extends Thread{
    private Object o1;
    private Object o2;

    public MyThread2(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        synchronized (o2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){

            }
        }
    }
}

是不是一上来就使用synchronized?

​ 不是,synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量(并发量)降低,

是在不得已的情况才选择的。

秒杀功能 12306 (排队就用户体验不好了),所有还是选择异步(并发)方式,但是不安全!

​ 第一种解决方案:尽量使用局部变量代替“实例变量”和“静态变量”。

​ 第二种方案:如果必须是实例变量,那么就考虑创建多个对象,这样实例变量的内存就不共享了

(一个线程对应一个对象,100个线程对应100个对象,对象不共享就没有数据安全问题了。)

​ 第三章方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了,线程同步机制。

线程这块还有哪些其他内容?
  1. 守护线程
  2. 定时器
  3. 实现线程的第三种方式:FutureTask方式,实现Callable接口。(JDK8新特性)
  4. 关于Object类中的wait和notify方法。
守护线程

java语言中的守护线程分为两大类:

​ 一类是:用户线程

​ 一类是:守护线程(后台线程)

​ 其中具有代表性的就是:垃圾回收线程(守护线程)

守护线程的特点:

​ 一般守护线程是个死循环,所有的用户线程只要结束,

守护线程自动结束。

注意:主程序main方法是一个用户线程。

守护线程用在什么地方呢?

守护线程的使用:

t.setDaemon(true);

/*
设置守护线程
 */
public class ThreadTest14 {
    public static void main(String[] args) {
        BakDataThread t = new BakDataThread();
        t.setName("备份数据的线程");

        //启动线程之前,将线程设为守护线程。
        t.setDaemon(true);

        t.start();

        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--->"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class BakDataThread extends Thread{
    @Override
    public void run() {
        int i=0;
        //即使是死循环,但由于该线程是守护线程,当用户线程结束,守护线程自动终止。
        while (true){
            System.out.println("计时器--->"+(i++));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
定时器

定时器的作用:间隔特定的时间去执行特定的程序。

可以使用sleep方法,睡眠,设置睡眠时间。(比较low)

在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用,这种方式也很少用

在实际的开发中,目前使用较多的是Spirng框架中提供的SpringTask框架,

在这个框架中只要进行简单的配置,就可以完成定时器的任务。

实现线程的第三种方式

实现Callable接口:

这种方式实现的线程可以获取线程的返回值,之前的那两种方式是无法获取返回值的,因为run方法返回的是void。

思考:系统委派一个线程去执行一个任务,这个线程执行完之后,可能会有体格执行结果,怎么获取这个执行结果呢?使用线程的第三种方式Callable接口。

优点:可以获取线程执行的返回值。

缺点:效率低,因为调用get()方法会导致当前线程阻塞。

/**
 * 实现线程的第三种方式:
 *      实现Callable接口
 */
public class ThreadTest15 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //第一步:创建一个"未来任务类"对象。
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {//call方法就相当于run方法,只不过这个有返回值。
                //线程执行一个任务,执行结束后可能有一个执行结果。
                //模拟执行
                System.out.println("call method begin");
                Thread.sleep(1000*10);
                System.out.println("call method over");
                int a = 100;
                int b = 200;
                return a+b;//自动装箱(300结果编程Integer)
            }
        });

        //创建线程对象
        Thread t = new Thread(task);
        t.setName("t");
        t.start();

        //这里是main方法,这是在主线程中。
        //在主线程中,怎么获取t线程的返回结果呢?
        //get()方法的执行会导致当前线程的阻塞。
        Object obj = task.get();
        System.out.println("线程执行结果"+obj);

        //main方法这里的程序想要执行必须等待get()方法的结束,
        //而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果。
        //另一个线程的执行是需要时间的。

        System.out.println("main over");
    }
}
Object类中的wait和notify方法

第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法。,因为这两个方法

是Object自带的。

Object o = new Object();

o.wait();

o.notify();

表示:

​ 让正在o对象上活动的线程进入等待状态,无期限等待。

​ 直到调用notify()方法唤醒线程对象。notifyAll()方法用来唤醒o对象上处于等待的所有

线程。

生产者与消费者模式均衡(难点)
/**
 * 1、使用wait方法和notify方法实现“生产者和消费者模式”
 * 2、什么是生产者和消费者模式?
 *     生产线程负责生产,消费线程负责消费。
 *         生产线程和消费线程要达到均衡,在这种特殊的情况下需要
 *     使用wait()和notify()方法,
 * 3、这两个该方法不是线程对象的方法,是普通java对象都有的方法。
 * 4、wait()方法和notify()方法建立在线程同步的基础之上。因为
 * 多线程要同时操作一个仓库,有线程安全问题。
 * 5、wait()方法的作用:o.wait()让正在o对象上活动的线程t进入等待状态,并释放t线程之前所占有的o对象的锁。
 * 6、notify()方法的作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前所占有的锁。
 * 7、模拟这样一个需求:
 *  仓库我们采用List集合。
 *  List集合中假设只能存1个元素。
 *  必须达到:生产1个消费1个。
 *
 */
public class ThreadTest16 {
    public static void main(String[] args) throws InterruptedException {
        List list = new ArrayList();
        Thread t1 = new Thread(new Producer(list));
        Thread t2 = new Thread(new Consumer(list));
        t1.setName("生产者线程");
        t2.setName("消费者线程");

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

//生产线程
class Producer implements Runnable{
    private List list;

    public Producer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        while (true){
            synchronized (list){
                if (list.size()>0){//大于0,说明仓库中已经有了一个元素。
                    try {
                        list.wait();//当前线程进入等待状态,并且释放list集合的锁。
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //程序能够执行到这里说明仓库是空的,可以生产。
                Object o = new Object();
                list.add(o);
                System.out.println(Thread.currentThread().getName()+"--->"+o);

                //唤醒消费者
                list.notifyAll();

            }
        }

    }
}

//消费线程
class Consumer implements Runnable{
    private List list;

    public Consumer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        while (true){
            synchronized (list){
                if (list.size()<=0){//仓库已空,需要生产,所以当前对象的消费线程要等待。
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //程序能够执行到这里,代表仓库已经满了,可以消费。
                Object obj = list.remove(0);
                System.out.println(Thread.currentThread().getName()+"--->"+obj);

                //唤醒生产者
                list.notifyAll();
            }
        }
    }
}

练习:使用生产者和消费者模式,交替输出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值