Java线程学习笔记

一. 概述

使用的线程的目的有如下几点:

  1. 异步。所谓异步,字义上来讲就是同时做多个不同的事。
    例如,你正在和恋人聊QQ,而此时你正在发送一个文件,如果收发消息和上传文件在同一个线程,那么当你发送文件开始,你便需要等待文件上传完成后,才能发送下一条消息,如果再加上文件太大、网速很差等等因素,就可能导致你和你恋人在几个小时内只说了一句话,那你们之间可能就GG了。
  1. 并发。所谓并发,字义上来讲就是同时做多个相同的事。
    例如,双11上淘宝买东西,在同一时间内,阿里的服务器收到一多个购买的请求,如果处理这些请求的方式是处理完一条再处理下一条的话,那么你买个衣服坑你就要等到地老天荒了。

虽然我在这里将线程的目的分成了两种,但它们的本质是一样:在同一时间做多个不同的事。值得注意的是,如果你电脑的CPU是单核单线程的话,这个“同一时间”是有事件差的,可能CPU这一毫秒在执行线程A,下一毫秒在执行线程B,但完全同一时间同时执行线程A和线程B是不可能的。

二. 如何创建线程

实现线程有两种方式,涉及到的类有两个,分别是:java.lang.Thread和java.lang.Runnable。创建并启动线程的方式如下:

Thread thread = new Thread();//创建线程实例
thread.start();//启动线程

但是这仅仅是创建了一条新的线程并启动了它,该线程并不会执行任何逻辑,那么我们怎么让它执行相关逻辑呢?

方式1:继承Thread类,重写run方法。

public class DemoThread extends Thread {
    public void run() {
        //TODO 线程中需要执行的相关逻辑
    }

    public static void main(String[] args) {
        Thread thread = new DemoThread();//创建线程实例
        thread.start();//启动线程
    }
}

方式二:实现Runnable接口,并在Thread构造方法中传入实现的Runnable实例。

public class DemoRunnable implements Runnable {
    public void run() {
        //TODO 线程中需要执行的相关逻辑
    }

    public static void main(String[] args) {
        Runnable runnable = new DemoRunnable();//创建一个实现了Runnable接口的类的实例
        Thread thread = new Thread(runnable);//创建线程实例,并在构造方法中传入实现了Runnable类的实例
        thread.start();//启动线程
    }
}

注: 这两种方法都能让线程执行我们需要执行的逻辑代码。当你调用start()函数启动线程后,程序会在该线程中调用Thread类自己的run()方法,如果你在创建线程时传入了Runnable的实例,那么在Thread类的run()方法中,会调用Runnable的run()方法。
特别需要注意的是,new Thread()只是创建了Thread类的一个实例,此时并没有创建出一条线程。而启动线程是调用start()方法而不是run()方法:直接调用run()方法时你只是调用的一个普通方法去执行相关逻辑代码,逻辑依然执行再你调用run()方法的线程中;而调用start()方法,jvm才会创建一条新线程,而此时run()才会在该线程中自动被回调执行。

三、线程同步

什么是同步?

同步是某个任务在同一时间只能由一个线程在执行,等这个线程执行完成后,下一个线程才能执行。那么既然线程的目的是为了异步,那么又为什么需要同步呢?这主要是由于数据安全造成的。多个线程在同时操作一个数据,当线程A还没没来得急使用该数据时,线程B就改了该数据的状态或值,这是就可能导致结果的错误,甚至是程序运行的异常。就像由此你和朋友都很饿,然后看到一个苹果,你正准备吃,然后你朋友直接给你抢来吃的还剩核,然后......例子可能不精确,见谅。

实现线程同步主要要使用两个点:锁和synchronized关键字。

1.锁。
锁是java中一种机制,分为对象锁和类锁。每个对象/类都有一个单一的锁(需要注意的是,一个类可以有多个对象,每个对象的锁也都是独立且单一的,互不干扰)。当一个线程获取到某个对象/类的锁后,除非该锁被释放,否则其他线程是不能获取到该对象/类的锁,而此时如果其他线程要获取该对象的锁,就只能等待。而上面所说的某个任务,便是获取到同一个锁的代码块,他可能里面的逻辑并不相同,但是获取的锁是相同的。

2、synchronized关键字用于需要同步的逻辑中,实现方式分为:同步方法和同步块。synchronized的目的就是获取某个对象/类的锁。分为:

同步块。其中的object参数便是你要获取的锁的对象。

synchronized (object) {
    //TODO 这里是同步块中需要执行的逻辑
}

对象同步方法。该synchronized获取到的锁是类A时候化后的对象的锁。

public class A {
    public synchronized void syncMethod() {
        //TODO 这里是同步方法中需要执行的逻辑
    }
}

类同步方法,即静态同步方法。该synchronized获取到的锁是类A的锁。

public class A {
    public static synchronized void staicSyncMethod() {

    }
}

同步的例子

public class ThreadSyncDemo {

    private static Object locker = new Object();//需要获取锁的对象

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (locker) {
                    for (int i = 0; i < 1000; ++i) {
                        System.out.println(i);
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (locker) {
                    for (int i = 0; i < 1000; ++i) {
                        System.out.println("aaa");
                    }
                }
            }
        });

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

执行这段代码时,你先将synchronized块去掉,保留里面的逻辑,然后执行,此时你会返现控制台输出中,数字和字母是交叉出现的,说明是异步执行的。然后加上同步快,再执行,你会发现此时数字先打印完后才打印的字母(或者字母先打印完后才打印的字母),说明是同步执行的。对象同步方法和静态同步方法原理的列子这里就不再举出,只要知道到底是获取到那个对象或类的锁,其他都是类似的。

死锁

死锁,顾名思义就是锁死了,执行不下去了。若有两个线程A和B,若线程A在执行时获取到对象X的锁,在同步块中又获取对象Y的锁,而此时线程B在执行时获取到对象Y的锁,而B的同步块中又在获取X的锁。再某种比较的极端情况下,A持有X的锁,B持有Y的锁,A执行到获取Y的锁时B未执行完,A阻塞等待,然后B又获取X的锁,而此时A还在阻塞等待B持有的Y的锁,未释放X的锁,导致B也阻塞等待。A和B都在阻塞等待,然后就没有然后了~~~~
所以避免死锁的其中之一便是尽量不要交叉获取锁。当然这不是唯一导致死锁的可能。
下面是一个死锁的列子:

public class ThreadSyncDemo {

    private static Object locker = new Object();

    public static void main(String[] args) {
        final Thread t1 = new Thread() {
            @Override
            @Deprecated
            public void run() {
                synchronized (locker) {//获取ThreadSyncDemo的类锁
                    for (int i = 0; i < 10000; ++i) {
                        System.out.println(i);
                        if (i == 1000) {
                            this.suspend();//挂起该线程,此方法暂停线程执行,但不会释放锁
                        }
                    }
                }
            }
        };

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (locker) {//获取ThreadSyncDemo的类锁
                    for (int i = 0; i < 10000; ++i) {
                        System.out.println("aaa");
                    }
                }
            }
        });

        t1.start();
        try{
            Thread.sleep(50)//暂停下,保证他t1先执行。
        }catch(Exception e){}
        t2.start();
    }
}

上面例子中,线程t1和线程t2的同步块都是获取的ThreadSyncDemo.class的锁,在t1同步块中,当i=1000时,暂停了t1线程的执行,此时t1的同步块未释放锁,而t2一直在等待t1释放锁,如果t1线程不继续执行,则t2也执行不了。

四. Thread类的相关方法

wait()与notify()/notifyAll()方法

实际上,wait()、notify()、notifyAll()这三个方法并不是Thread类的专有方法,而是Object的方法,也就是说,每个对象都存在这三个方法。

需要注意的是,这三个方法都只能在同步块/同步方法中执行,其他地方执行时没有意义的。而且需要同步块中获取到的锁的对象来调用才有效果。

wait()顾名思义让同步块暂停执行并等待,此时该同步块会让出获取到的锁,让其他线程执行获取同一把锁的同步块。而在其他线程执行完后,调用notify()/notifyAll()方法,之前等待的的同步块就会继续执行。但是,如果调用了notify()/notifyAll()之后,后面有长时间任务二导致锁未被释放,等待中的同步块也需要等锁被释放后才会继续往下执行。如果同一个锁有多个地方等待,就需要使用notifyAll()来全部唤醒,使他们重新争夺锁的行列中,谁先获取到锁就谁先执行。注意如果等待的是多个,nofity()和nofityAll()后最先获取的锁的是那个,由jvm决定。
wait()方法有两个个重载方法,wait(long timeout)和wait(long timeout, int nanos),其中timeout是等待时间,如果timeout=0,则表示一直等待知道nofity()/nofityAll()被调用切获取到锁后继续执行,timeout>0则表示等待多少时间后,只要获取到锁就继续执行。至于nanos,表示纳秒,值在0-999999之间,为了更好的控制时间。

public class ThreadSyncDemo {

    private static Object locker_1 = new Object();

    public static void main(String[] args) {
        final Thread t1 = new Thread() {
            @Override
            @Deprecated
            public void run() {
                synchronized (locker_1) {
                    for (int i = 0; i < 1000; ++i) {
                        System.out.println(i);
                        if (i == 100) {
                            try {
                                locker_1.wait();//当i=100是,执行wait()方法,释放锁,进入等待状态
                            } catch (Exception e) {
                            }
                        }
                    }
                }
            }
        };

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (locker_1) {
                    for (int i = 0; i < 100; ++i) {
                        System.out.println("aaa");
                    }
                    locker_1.notify();//当执行完后,通知等待的线程继续执行。
                }
            }
        });

        t1.start();
        try{
            Thread.sleep(50) //保证t1先执行
        }catch(Exception e)
        t2.start();
    }
}

上例,控制台先会打印0-100,当线程t1中i=100时,调用locker_1.wait()使其释放锁并进入等待状态,此时线程t2获取到锁,控制台打印100个aaa,然后调用locker_1.notify()后,线程t1会继续在控制台打印101-999。

interrupt(),interrupted(),isInterrupt()方法

interrupt()方法看起来是中断线程,但实际上,当你调用interrupt()方法后,你发现线程该干嘛还是再干嘛,除非你的线程中存再调用sleep()、wait()等方法的时候,此时会抛出InterruptedException异常,以供手动处理线程停止。isInterrupt()返回线程是否是中断状态。interrupted()方法是类方法。从Thread类的源码看,isInterrupt()和interrupted()都会调用以下方法:
private native boolean isInterrupted(boolean ClearInterrupted);
不同是isInterrupt()传入的ClearInterrupted=false,
interrupted()方法传入的ClearInterrupted=true,当ClearInterrupted=true时,线程的中断状态会被清除,也就是说此时isInterrupt()方法的返回值是false。

suspend()与resume()方法

已废弃的方法,用于暂停和重新开始执行线程。和wait()不同的是,suspend()是通过线程的实例调用的,而不是锁对象,调用也不需要在同步块中调用,而且suspend()方法调用后并不会释放锁。列子:

public class ThreaSuspendResumeDemo {

    public static void main(String[] args) {
        final Timer timer = new Timer();

        final Thread t = new Thread() {
            @Deprecated
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    System.out.println(i);
                    if (i == 1000) {
                        this.suspend();//暂停线程
                    }
                }
                timer.cancel();
            }
        };
        t.start();

        timer.schedule(new TimerTask() {
            @Override
            @Deprecated
            public void run() {
                t.resume();//3秒后将线程唤醒
            }
        }, 3000);
    }
}

stop()方法

废弃的方法。暴力终止线程,调用此方法后,线程中未执行的语句将不会再执行。但是isAlive()方法和isInterrupt方法的返回值依然是false,所以,暴力如此,想想都可怕,谨慎使用。


public class ThreadStopDemo {

    public static void main(String[] args) {

        final Thread t = new Thread() {
            @Deprecated
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    System.out.println(i);
                    if (i == 1000) {
                        this.stop();//i=1000时就停止了,控制台只会打印0-1000。
                    }
                }
                System.out.println("Is this thread alive?" + this.isAlive());//线程已经终止,这条语句是不会执行的
            }
        };
        t.start();

        final Timer timer = new Timer();
        timer.schedule(new TimerTask(){
            @Override
            @Deprecated
            public void run() {
                System.out.println("Is this thread alive?" + t.isAlive());//false
                System.out.println("Is this thread interrupt?" + t.interrupted());//false
            }
        }, 3000);
    }
}

destroy()方法

额,为什么要讲这个方法呢?因为我以为会和stop()方法一样的丧心病狂,但是我错了。从Thread.destroy()方法的源码来看,结果让人发呆流鼻涕。源码如下:

    /**
     * Throws {@link NoSuchMethodError}.
     *
     * @deprecated This method was originally designed to destroy this
     *     thread without any cleanup. Any monitors it held would have
     *     remained locked. However, the method was never implemented.
     *     If if were to be implemented, it would be deadlock-prone in
     *     much the manner of {@link #suspend}. If the target thread held
     *     a lock protecting a critical system resource when it was
     *     destroyed, no thread could ever access this resource again.
     *     If another thread ever attempted to lock this resource, deadlock
     *     would result. Such deadlocks typically manifest themselves as
     *     "frozen" processes. For more information, see
     *     <a href="{@docRoot}/../technotes/guides/concurrency/threadPrimitiveDeprecation.html">
     *     Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?</a>.
     * @throws NoSuchMethodError always
     */
    @Deprecated
    public void destroy() {
        throw new NoSuchMethodError();
    }

是不是亮瞎了钛合金狗眼!!!!!

五.守护线程

线程分为用户(User)线程和守护(Daemon)线程,守护(Daemon)线程的实现就是线程在调用start()方法前,先调用setDaemon(true)方法。区别是,普通用户(User)线程,只要线程还在执行,那么程序就永远不会退出;而守护(Daemon)线程只要程序主线程执行完后,守护(Daemon)线程也就被终止。守护(Daemon)线程常作为辅佐的的作用。

以上便是此次线程学习的第一部分总结,如有意见或建议欢迎提出,相互探讨才能共同成长与进步。后面讲继续研究线程池和线程调度。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容