【线程、同步】

【线程、同步】

线程的创建

创建多线程程序(一)

创建多线程程序的第一种方法——创建Thread类的子类。

实现步骤:

  1. 创建一个Thread类的子类

  2. 在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)

  3. 创建Thread类的子类对象

  4. 调用Thread类中的方法start(),开启新的线程,执行run方法

    void start() 使该线程开始执行:JVM调用该线程的run方法

    结果是两个线程并发地运行,当前线程(main线程)和另一个线程(创建的新线程,执行其run方法)

    多次启动一个线程是非法的,特别是当前程已经结束执行后,不能在重新启动。

//1.创建一个Thread类的子类
public class MyThread extends Thread {
    //2.在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
    @Override
    public void run(){
        for (int i = 0; i < 10; i++) {
            System.out.println("run"+ i);
        }
    }
}
public class Demo04Thread {
    public static void main(String[] args) {
        //3.创建Thread类的子类对象
        MyThread myThread = new MyThread();
        //4.调用Thread类中的方法start(),开启新的线程,执行run方法
        myThread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main" + i);
        }
        //run0
        //main0
        //run1
        //main1
        //run2
        //main2
        //run3
        //main3
        //run4
        //main4
        //run5
        //main5
        //run6
        //main6
        //run7
        //main7
        //run8
        //main8
        //run9
        //main9
    }
}

Thread类的常用方法

获取线程名称的方法
  1. 使用Thread类中的方法getName():

    String getName() 返回该线程的名称。

  2. 可以先获取到当前正在执行的线程,使用使用线程中的方法getName()获取线程的名称

    static Thread currentThread() 返回对当前正在执行的线程对象的引用

线程的名称:

主线程:main

新线程:Thread-0,Thread-1,Thread-2…

//定义一个Thread子类
public class MyThread extends Thread{
    //重写Thread类中的run方法,设置线程任务
    @Override
    public void run(){
        //通过getName方法获取线程的名称
        String name = getName();
        System.out.println(name);
        //通过当前Thread对象调用getName方法获取线程的名称
        Thread t = Thread.currentThread();
        String name1 = t.getName();
        System.out.println(name1);
    }
}
public class Demo01Thread {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
        new MyThread().start();
        new MyThread().start();
        //获取main线程的名称
        System.out.println(Thread.currentThread().getName());
    }
}
设置线程名称的方法(了解)
  1. 使用Thread类中的方法setName():

    void setName(String name) 改变线程的名称,使之与参数name相同。

  2. 创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参数构造方法,把线程的参数传递给父类,让父类(Thread)给子线程起一个名字:

    Thread(String name) 分配新的Thread对象

//定义一个Thread子类
public class MyThread extends Thread{
    public MyThread(){

    }
    //创建带参数的构造方法,设置线程名称
    public MyThread(String name){
        super(name);
    }
    //重写Thread类中的run方法,设置线程任务
    @Override
    public void run() {
        //获取线程的名称
        System.out.println(Thread.currentThread().getName());
    }
}
public class Demo01Thread {
    public static void main(String[] args) {
        //开启多线程
        MyThread mt = new MyThread();
        //修改线程名称,方法一:
        mt.setName("James");
        mt.start();
        //开启多线程,设置线程名称
        new MyThread("Kobe").start();

    }
}
sleep方法

public static void sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)

毫秒数结束之后,线程继续执行

public class Demo01Sleep {
    public static void main(String[] args) {
        //模拟秒表
        for (int i = 1; i <= 60; i++) {
            System.out.println(i);
            //使用Thread类的静态方法sleep方法让程序睡眠1秒钟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

###创建多线程程序(二)

创建多线程程序的第二种方式:实现Runnable接口。

java.lang.Runnable

Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个成为run的无参数方法

java.lang.Thread类的构造方法

Thread(Runnable target) 分配新的Thread对象

Thread(Runnable target, String name) 分配新的Thread对象

实现步骤:

  1. 创建一个Runnable接口的实现类
  2. 在实现类中重写Runnable接口的run方法,设置线程任务
  3. 创建一个Runnable接口的实现类对象
  4. 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
  5. 调用Thread类中的start方法,开启新的线程执行run方法
public class Demo01RunnableThread {
    public static void main(String[] args) {
        Runnable rt = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        };
        new Thread(rt).start();//Thread-0
        new Thread(rt,"James").start();//James
    }
}

Thread和Runnable的区别

实现runnable接口创建多线程程序的好处:

  1. 避免了单继承的局限性;
    • 一个类只能继承一个类,类继承了Thread类就不能继承其他的类
    • 实现Runnable接口,还可以继承其他的类,实现其他的接口
  2. 增强了程序的扩展性,降低了程序的耦合性(解耦)
    • 实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
    • 实现类中,重写了run方法:用来设置线程任务
    • 创建Thread类对象,调用start方法:用来开启新线程

匿名内部类方式实现线程的创建

匿名:没有名字

内部类:写在其他类内部的类

匿名内部类的作用:简化代码。

  • 把子类继承父类,重写父类的方法,创建子类对象,合为一步完成;
  • 把实现类实现接口,重写接口中的方法,创建实现类对象,合为一步完成。

匿名内部类的最终产物:子类/实现类对象,而这个类没有名字。

格式:

new 父类/接口(){

​ 重写父类/接口中的方法

}

public class InnerClassThread {
    public static void main(String[] args) {
        //线程的父类是Thread
        new Thread(){
            @Override
            public void run(){
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+":James");
                }
            }
        }.start();
        //线程的接口Runnable
        Runnable r =new Runnable(){
            @Override
            public void run(){
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+":Kobe" );
                }
            }
        };
        new Thread(r).start();
        //链式编程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+":King");
                }
            }
        }).start();
    }

}

线程的安全问题

对于电影院卖票,一个窗口中卖100张票,一张一张的卖,不会产生问题:相当于,单线程程序是不会出现线程安全问题的。

如果电影院中有多个窗口,每个窗口中卖的票种不同,也不会出现问题:相当于,多线程程序,没有访问共享数据,不会出现问题。

如果电影院中的多个窗口,每个窗口中都能卖所有的票种,当不同窗口都卖同一张票时,就会产生问题:相当于,多线程程序,访问共享数据,会产生线程安全问题。

线程安全问题的代码实现

//实现卖票案例
public class RunnableImpl implements Runnable {
    //定义一个多个线程共享的票源
    private int tickets = 100;
    @Override
    public void run() {
        //票数大于0时,进入循环
        while (tickets>0)  {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票,tickets--
                System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
                tickets--;

        }
    }
}
//模拟卖票案例。
//创建3个线程,同时开启,对共享的票进行出售
public class Demo02Tickets {
    public static void main(String[] args) {
        Runnable r = new RunnableImpl();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        Thread t3 = new Thread(r);
        t1.start();
        t2.start();
        t3.start();
    }
}

解决线程安全问题——线程同步

当我们使用多个线程访问统一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票的问题,Java中提供了同步机制(synchronized)来解决。

根据案例简述:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口 1操作结束,窗口1、窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU对的资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

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

有三种方式完成同步操作:

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

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

格式:

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

同步锁:

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

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

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

//实现卖票案例
public class RunnableImpl implements Runnable {
    //定义一个多个线程共享的票源
    private int tickets = 100;
    Object obj = new Object();
    @Override
    public void run() {
        //票数大于0时,进入循环
        while (tickets > 0) {
            synchronized (obj) {
                if ((tickets > 0)) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //票存在,卖票,tickets--
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
                    tickets--;
                }
            }
        }
    }
}

同步技术的原理:

使用了一个锁对象,这个锁对象叫做同步锁,也叫做对象监视器。

3个线程一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票。

  • t0抢到了cpu的执行权,执行run方法,会遇到synchronized代码块;这时t0会检查同步代码块是否有锁对 象;发现有,就会获取到锁对象,进入到同步中执行
  • t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象;发现没有,t1就会进入到阻塞状态,会一直等待t0线程归还锁对象,一直到t0线程执行完同步中的代码块,会把锁对象归还给同步代码块,t1才能获取到锁对象进入到同步中执行

总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。

同步保证了只能有一个线程在同步中执行共享数据,保证了安全性。但是程序频繁的判断锁、获取锁、释放锁,程序的效率会降低。

2.同步方法

使用步骤:

  1. 把访问了共享数据的代码抽取出来,放到一个方法中;
  2. 在方法上添加synchronized修饰符。

格式:定义方法的格式。

修饰符  synchronized  返回值类型  方法名(参数列表){
    可能会出现线程安全问题的代码(访问了共享数据的代码)
}

**注意:**定义一个同步方法,同步方法也会把方法内部的代码锁住,只让一个线程执行。同步方法的锁对象就是实现类对象:new RunnableImpl,也就是this。

代码示例:

//实现卖票案例
public class RunnableImpl implements Runnable {
    //定义一个多个线程共享的票源
    private int tickets = 100;
    
    @Override
    public void run() {
        //票数大于0时,进入循环
        while (tickets > 0) {
           method();
        }
    }

    //定义一个同步方法
    public synchronized void method() {
        if ((tickets > 0)) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //票存在,卖票,tickets--
            System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
            tickets--;
        }
    }
}
2.1 静态同步方法(了解)

静态的同步方法,锁对象就不能是this了。(this是创建对象之后产生的,静态方法优先于对象)

静态方法的锁对象是本类的class属性,也叫class文件对象。

//实现卖票案例
public class RunnableImpl implements Runnable {
    //定义一个多个线程共享的票源
    private static int tickets = 100;

    @Override
    public void run() {
        //票数大于0时,进入循环
        while (tickets > 0) {
            methodStatic();
        }
    }

    //定义一个同步方法
    public static synchronized void methodStatic() {
        if ((tickets > 0)) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //票存在,卖票,tickets--
            System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
            tickets--;
        }
    }
}

静态同步方法内藏的锁对象为:本类的class属性。

//实现卖票案例
public class RunnableImpl implements Runnable {
    //定义一个多个线程共享的票源
    private static int tickets = 100;

    @Override
    public void run() {
        //票数大于0时,进入循环
        while (tickets > 0) {
            methodStatic();
        }
    }

    //定义一个同步方法
    public static void methodStatic() {
        synchronized (RunnableImpl.class) {
            if ((tickets > 0)) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票,tickets--
                System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
                tickets--;
            }
        }

    }
}
3. Lock锁

java.util.concurrent.locks.Lock接口提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁也叫做同步锁,加锁和释放锁方法化了,如下:

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

Lock接口有一个实现类ReentrantLock类,可以实现Lock接口的的各种方法。

使用步骤:

  1. 在成员位置创建一个ReentrantLock对象;
  2. 在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁;
  3. 在可能会出现安全问题的代码后调用unLock接口中的unLock释放锁。

代码示例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//实现卖票案例
public class RunnableImpl implements Runnable {
    //定义一个多个线程共享的票源
    private static int tickets = 100;
    Lock l = new ReentrantLock();

    @Override
    public void run() {
        //票数大于0时,进入循环
        while (tickets > 0) {
            //在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁;
            l.lock();
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();

                } finally {
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
                    tickets--;
                    l.unlock();
                }
            }
        }
    }
}

线程的状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也并不是一直处于执行状态。在线程的生命周期中,在API中java.lang.Thread.State这个枚举中给出了六种线程状态。

线程状态导致状态发生的条件
NEW(新建)线程刚被创建,但是并为启动。还没有调用start方法
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器
Blocked(锁阻塞)当一个想成尝试获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能唤醒
Timed Waiting(计时等待)同Waiting状态,有几个方法有超时参数,调用他们讲进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep、Object.wait
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡

Timed Waiting(计时等待)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GboBLIYF-1590548938061)(C:\Users\Ann\Desktop\javaLearning\笔记\picture\计时等待.png)]

需要记住以下几点:

  1. 进入Timed Waiting状态的一种常见情形是调用sleep方法,单独的线程也可以调用,不一定非要有协作关系;
  2. 为了让其他线程有机会执行 ,可以将Thread.sleep()的调用放在线程run()之内。这样才能保证该线程执行过程中会睡眠;
  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。

小提示:sleep()中指定的时间是线程不会运行的最短时间。因此,sleep方法不能保证该线程睡眠到期后就能开始立刻执行。

Blocked(锁阻塞)

Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEOCWvg0-1590548938061)(C:\Users\Ann\Desktop\javaLearning\笔记\picture\锁阻塞.png)]

Waiting(无限等待)

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

等待唤醒案例:线程之间的通信。

  • 创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行权,进入到Waiting状态(无限等待);
  • 创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子。

注意:

  1. 顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行。
  2. 同步使用的锁对象必须保证唯一。
  3. 只有锁对象才能调用wait和notify方法。

Object类中的方法 :

代码含义
void wait()在其他线程调用此对象的notify()或者notifyAll()方法之前,导致当前线程等待
void notify()唤醒在此对象监视器上等待的单个线程,会继续执行wait方法之后的代码
public class WaitAndNotify {
    public static void main(String[] args) {
        //创建锁对象,保证唯一
        Object obj = new Object();
        //创造一个消费者,使用匿名内部类,匿名对象
        new Thread(){
            @Override
            public void run(){
                //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                synchronized(obj){
                    System.out.println("告知老板要的包子的种类和数量");
                    try {
                        //调用wait方法,放弃cpu的执行权,进入到Waiting状态(无限等待)
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //唤醒之后执行的代码
                    System.out.println("顾客吃包子");
                }
            }
        }.start();
        //创造一个生产者,使用匿名内部类,匿名对象
        new Thread(){
            @Override
            public void run(){
                //花了五秒钟做包子
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                synchronized(obj){
                    System.out.println("老板花了5秒钟之后做好了包子,告知顾客可以吃包子了");
                    obj.notify();
                }
            }
        }.start();
    }
}

补充:Object类中wait带参方法和notifyAll方法

进入到Timed Waiting(计时等待)有两种方式:

  1. 使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
  2. 使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notif唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态

唤醒的方法:

  1. notify():唤醒在此对象监视器上等待的单个线程;
  2. notifyAll():唤醒在此对象监视器上等待的所有线程。

一条有意思的tips:

我们在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的, 比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。

这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是

如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两 得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒 计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。

  }
        }
    }.start();
}

}


### 补充:Object类中wait带参方法和notifyAll方法

进入到Timed Waiting(计时等待)有两种方式:

1. 使用sleep(long  m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
2. 使用wait(long  m)方法,wait方法如果在毫秒值结束之后,还没有被notif唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态

唤醒的方法:

1. notify():唤醒在此对象监视器上等待的单个线程;
2. notifyAll():唤醒在此对象监视器上等待的所有线程。

> 一条有意思的tips:
>
> 我们在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的, 比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。
>
> 这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是
>
> 如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两 得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒 计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值