[Java基础复习] 线程实现

线程实现的三种方法

1. 继承Thread

这种方法是继承,接下来实现run方法,在方法下写上作为线程被调用的时候需要运行的代码。
注意下面的代码,调用MyThread成为线程,并且start()以后,只会运行run里面的代码。除非你直接运行MyThread,不然main方法是不会运行的。

public class MyThread extends Thread {
    @Override
    public void run() {
        for(int i = 0; i < 10; i++)
            System.out.println(i);
    }

    public static void main(String[] args) {
        System.out.println(123);
    }
}


public class Demo1 {
    public static void main(String[] args) {

        MyThread m = new MyThread();
        m.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("汗滴禾下土"+i);
        }
    }
}

run方法描述的就是你这个线程在运行的时候要去做的事,而不是说这个路径的触发方式。所以是thread.start(),不是thread.run()。所以它们的关系是使用start()方法之后,这个thread会去跑run()里面的代码。

下面列一张图,大致描述两个线程运行的过程。进程结束的一条原因就是它的所有线程全部结束了。
在这里插入图片描述
注意,在一个线程里面所执行的方法是跑在这个线程自己的栈里的。我们之前提到,每个线程都是有自己的独立栈空间,而方法又是加载在栈里的,这么说也就不难理解了。

2. 实现接口Runnable

Runnable的实现更偏向于是创建任务,而不是直接创建线程。先来看一下代码。这里同样的,MyThread在start之后也不会运行main函数。

public class MyThread implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i < 10; i++)
            System.out.println(i);
    }

    public static void main(String[] args) {
        System.out.println(123);
    }
}

public class Demo1 {
    public static void main(String[] args) {
		// 创建MyThread任务m
        MyThread m = new MyThread();
        // 创建线程t1,t2,并且都传入任务m
        Thread t1 = new Thread(m);
        Thread t2 = new Thread(m);
        //线程运行
        t1.start();
        t2.start();
        
        for (int i = 0; i < 10; i++) {
            System.out.println("汗滴禾下土"+i);
        }
    }
}

这里主要的区别是,先new了一个MyThread对象,然后把这个对象传到Thread t里面,再让t.start()。这里的好处之一是让程序更健壮,因为创建线程和执行任务是分离的。
注意一下,这里t1和t2用的都是拿m这个对象,因此他们操作的都是同一块内存。所以这时候就可能会涉及到多线程安全性问题了。这个我们后面说。

总结一下实现Runnable与继承Thread相比有如下优势:

1.通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况。比如都执行这个循环,我写一遍MyThread就好了,然后创建多个Thread对象,传入这个任务即可,不需要实现多个Thread类。
2. 可以避免单继承所带来的局限性。因为Thread继承以后就不能再继承其他类了,但是Runnable是接口,还可以允许继承其他类。
3. 任务与线程是分离的,提高了程序的健壮性
4. 线程池技术接受Runnable和Callable类型的任务,不接受Thread类型的线程

3. Callable

Callable 是JDK1.5之后出现的(Runnable是1.1),算是Runnable的进一步升级。它也是被视作任务,但是它的核心是call()方法,而不是run()方法,并且允许返回值以及异常处理。这些都是Runnable和Thread不允许的。看一下代码

//返回String类型
//注意这里implements Callable<T>
class CallDemo implements Callable<String>{
	@Override
	public String call() throws Exception {
		return null;
	}
}

class CallDemo implements Callable<T>{
	@Override
	public T call() throws Exception {
		return null;
	}
}

//申明方式
Callable<T> c = new Callable<T>(); //1
FutureTask<T> task = new FutureTask<T>(c); //2
Thread t = new Thread(task); //3
这里提一嘴他很好用的一个方法,就是get(),获取返回值。
如果使用了这个,那么相当于一个阻断,在这个之前的线程必须先运行完,下面的线程才能运行。
比如:
int i = t.get(); // 4 假设123里的T是Integer
for(...) //5
那么这里4必须要执行完才会去执行5

Thread类

1.常用构造方法有

  1. Thread()
  2. Thread(String name)
  3. Thread(Runnable/ Callable demo)
  4. Thread(Runnable/ Callable demo, String name) //为线程命名

常用方法

  1. start() //启动线程

  2. sleep(long millis) //让线程沉睡指定毫秒

  3. getName() //获取线程名称,也可以setName()

  4. getId() //获取线程id

  5. getPriority() //获取此线程优先级

  6. setPriority() //设置优先级 MAX_PRIORITY > NORM_PRIORITY > MIN_PRIORITY

  7. getState() //获取状态

  8. getThreadGroup() //查看位于哪一个线程池

  9. Thread.yield() //运行状态下的线程 调用Thread.yield()进入就绪状态后,和其它就绪状态线程处于同一起跑线,也有可能被立即再次被调用;

  10. wait() // 这个方法是让当前synchronized释放上锁的object。调用方法object.wait()

  11. notify() // 这个方法是唤醒下一个等待的资源的线程。调用方法obj.notify()

  12. notifyAll() // 这个方法是唤醒所有等待该资源的线程。调用方法obj.notifyAll()
    当程序不明确知道下一个要唤醒的线程时,需要采用notifyAll()唤醒所有在wait池中的线程,让它们竞争而获取资源的执行权,但使用notifyAll()时,会出现死锁的风险,因此,如果程序中明确知道下一个要唤醒的线程时,尽可能使用notify()而非notifyAll()。

  13. interrupt() //这种方法是比较安全的终止线程的方法。现在的java让线程能够安全结束的方法就是给线程上标记。线程在运行的时候在特定的情况下会去检查标记,一般在sleep的时候或者wait的时候(这两个会抛出InterruptedException)。因为使用这两个方法必须try catch,而有interrupt标记的时候会让他们进入catch块。这时候可以让他们安全关闭释放资源,也可以简单做其他处理,比如输出等。

这里注意一下,一般来说有三种中断线程的方法,有用一个变量中断,还有interrupt比起interrupt(),stop()方法和suspend()方法已经过时。原因是stop会让当前运行线程强制停止,如果这个线程尚未释放资源,强制停止等于把资源锁死。比如当前线程操作IO,直接stop会一直占用IO资源。而suspend则是容易造成死锁

下面看一下interrupt的代码:

public class Demo5 {
    public static void main(String[] args) {
        //线程中断
        //y一个线程是一个独立的执行路径,它是否结束应该由其自身决定
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //给线程t1添加中断标记
        t1.interrupt();
    }

    static class MyRunnable implements Runnable{

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                    System.out.println("发现了中断标记,线程自杀");
                    //线程自杀就直接return,这样run方法就结束了。如果不return程序会直接继续走,走完这个for循环。
                    return;
                }
            }
        }
    }
}

2. Synchronized浅析(隐式锁)

首先Synchronized只有两个锁的写法,一种是用锁代码块的写法,一种是直接锁方法的写法。而类锁一般对应代码块里直接synchronized(*.class)或者申明静态synchronized方法

1.修饰语句块的时候
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执⾏ monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象 头中,synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执⾏ monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞 等待,直到锁被另外⼀个线程释放为⽌。

2.修饰方法
由于是修饰方法,因此并不需要monitorenter和exit。取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法,JVM 通过该 ACC_SYNCHRONIZED 访问 标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤。

代码写法

1、修饰代码块(就是在代码块的花括号前面写上synchronized())
在这里插入图片描述

2、修饰指定对象的代码块
此时此刻,当一个线程访问a的对象时,其他试图访问此对象的线程将会阻塞,直到该线程访问account对象结束
在这里插入图片描述
这里加一段比较有意思的问题,这段代码里,如果我在run()的方法内部申明Object的话,就无法完成同步。是因为当t1 t2他们start()以后,都会各自调用 run方法,而run方法每次调用都会新创建一个Object。这样子就相当于每个人都有自己的一把锁,当然无法锁住。这里两种方法:一种是改成this,一种是把Object放在run外面申明。因为放在run外面的话Object只有一次实例化,就是Runnable r = new Ticket();三把锁都对应这个Object。

public class MyThread {
    public static void main(String[] args) {
        Runnable r = new Ticket();
        new Thread(r).start();
        new Thread(r).start();
        new Thread(r).start();
    }

    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        private Object o = new Object();
        @Override
        public void run() {
            //Object o = new Object();    //这里不是同一把锁,所以锁不住
            while (true) {
                synchronized (o) {
                    if (count > 0) {
                        //卖票
                        System.out.println("正在准备卖票");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
                    }else {
                        break;
                    }

                }
            }
        }
    }
}

3、修饰指定类的代码块
在这里插入图片描述

4、修饰静态方法和动态方法的代码块:
public (static) synchronized void method()
都是public synchronized …
但是静态方法因为在一开始就会被加载到内存,所以静态方法代码块就相当于给类上锁
而动态方法其实就是synchronized(this),锁定新创建的对象。跟之前的this代码块很像,但是底层方法还是有一点差别

3. Reentranslock浅析(显式锁)

这是lock的子类。这个锁跟synchronized在代码上的区别就是,sync锁定和解锁由方法块自定义。而lock需要自己在语句上定义lock.lock(),解锁就是lock.unlock()

4. 公平锁和非公平锁 以及 可重入锁

参考这篇,写的非常详细生动

5. 线程的六种状态
  1. New, 尚未启动的新线程,相当于在new Thread()这步
  2. Runnable,在JVM中运行的线程
  3. Blocked,被阻塞等待Monitor锁定的线程(这里涉及到sync底层机制,有兴趣的可以去搜一下,不然就简单理解为在等待解锁的排队中)
  4. Waiting,无限期等待另一个线程的操作中的状态,需要被唤醒
  5. Timed_waiting,计时等待,到时苏醒。
    4和5唤醒以后理想情况都会进入runnable状态,也有可能被阻塞变成blocked
  6. Terminated,线程结束状态

6. 生产者和消费者问题(著名的线程唤醒和沉睡问题)

这个代码的目的是让cook做100次菜,服务员取100次菜,并且按照顺序打印,一次老干妈香辣,一次煎饼果子甜辣…
先上一下正确的代码

public class Demo12 {
    public static void main(String[] args) {
        //多线程通信    生产者与消费者问题
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }
    //厨师
    static class Cook extends Thread{
        private Food f;

        public Cook(Food f) {
            this.f = f;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                if(i%2==0){
                    f.setNameAndTaste("老干妈小米粥","香辣味");
                }else {
                    f.setNameAndTaste("煎饼果子","甜辣味");
                }
            }
        }
    }
    //服务员
    static class Waiter extends Thread{
        private Food f;

        public Waiter(Food f) {
            this.f = f;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }
    //食物
    static class Food{
        private String name;
        private String taste;
        //true表示可以生产
        boolean flag = true;
        public synchronized void setNameAndTaste(String name,String taste){
            if(flag){
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
        public synchronized void get(){
            if(!flag){
                System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
                flag = true;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

接下来说一下我遇到的问题,与过程的演变。
下面的代码是最初始的代码,没有锁也没有其他控制机制。所以会有问题。这个问题是什么呢?

static class Food{
    private String name;
    private String taste;
    //true表示可以生产
    boolean flag = true;
    public void setNameAndTaste(String name,String taste){     
        this.name = name;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.taste = taste;
    }
    public void get(){  
        System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
    }
}

就是比如我cook在setName的时候,waiter进来了。因为setName和setTaste并不是原子操作,所以可能cook刚设置完name,waiter就调用get打印了。所以food这个对象的name更新了,但是口味还没更新,所以就会打印出”老干妈小米粥,甜辣味“。

于是Food类更新,出了2.0。那直接给两个方法上锁岂不就好了?但是这样一样会有问题。问题是什么呢?
问题就是虽然上锁了,但是因为java是优先调度资源,会抢占资源。比如cook拿到了锁,它结束以后比waiter抢占资源快,就会一直调用,那么就会导致cook先循环完100次,那么food就固定是煎饼果子甜辣了,而waiter后面运行通过get输出的就全是这个了。

static class Food{
    private String name;
    private String taste;
    //true表示可以生产
    boolean flag = true;
    public synchronized void setNameAndTaste(String name,String taste){     
        this.name = name;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.taste = taste;
    }
    public synchronized void get(){  
        System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
    }
}

那么到底该如何解决?首先sync不变,但是定义一个boolean变量flag。这个flag是true的时候说明可以生产,false的时候说明可以端菜。这样子我在生产以后把flag变成false,那就算cook再抢到资源进来发现是false也不能继续做菜了,只能等waiter进来get以后,把flag置换成true才能继续做菜。同时这里很巧妙的运用了一个wait()。这是因为就算cook进来发现flag是false可是他还是能一直进来抢占资源,这样会导致waiter很久以后才能拿到锁执行。可是如果加了一个wait(),那么这个线程在调用一次方法后就会沉睡,需要等其他线程拿到锁以后将他们唤醒。这里的this.notifyAll()指的是我把调用这个对象的所有线程全部唤醒。注意这里food是对象而不是线程,其他两个才是线程

static class Food{
        private String name;
        private String taste;
        //true表示可以生产
        boolean flag = true;
        public synchronized void setNameAndTaste(String name,String taste){
            if(flag){
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
        public synchronized void get(){
            if(!flag){
                System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
                flag = true;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值