Java多线程基础——多线程实例

前言

  在之前我们讲述了Java的线程模型,理解清楚了过后再我们使用的过程中才能得心应手,防止不必要的错误出现,多线程错误是很难复现的错误,一定要小心谨慎的使用。
  同时,这里讲的是线程间交互,同步的问题,如果线程间不存在交互,各自用自己的局部变量工作,也不存在这些问题了。

共享变量

假如有一下场景,两个线程依次对某一个成员变量进行操作,会出现什么问题呢?

public class Main {
        static int num;
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread() {
                    @Override
                    public void run() {
                        for (int j = 0; j < 1000; j++) {
                            num = j;
                        }
                        System.out.println(Thread.currentThread().getName() + ": num = " + num);
                    }
                }.start();
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main, num = " + num);
        }
}

我们可以发现num的最终值是999,而且每个线程的值都是999。我们发现这里并没有出现多线程的错误,其实是因为在Java里面基本类型的赋值操作是原子性的,上一节我们讲过。那假如吧赋值操作改为非原子性的操作呢?,比如改为num++,会怎么样呢?

       for (int j = 0; j < 1000; j++) {
           num ++;
       }

        //输出
        Thread-0: num = 1889
        Thread-1: num = 2000
        Thread-2: num = 3000
        Thread-4: num = 4000
        Thread-3: num = 5000
        Thread-5: num = 6000
        Thread-9: num = 7000
        Thread-8: num = 8046
        Thread-7: num = 8046
        Thread-6: num = 9046
        main, num = 9046

具体数据肯定有差别,发现这里出现了问题,结果并不是10000,这是由于num++是非原子操作,它包括3个操作:读取num的值,进行加1操作,把新值写入num。假如当前线程先读取了num的值放入工作内存,然后线程这是被切换到了另一个线程,另一个线程修改了num的值;在回到当前线程继续执行,这是的num就不是最新的值,所以导致出错。
那我们加上volatile关键字会怎么样呢?

volatile static int num;

其实不用看结果我们也能知道volatile也没用,它不能保证原子性,那么我们该怎么保证同步呢。这就轮到synchronized登场了。

synchronized

我们把Thread的run改成如下形式

        new Thread() {
            @Override
            public void run() {
                synchronized (Main.class) {
                    for (int j = 0; j < 1000; j++) {
                        num++;
                    }
                    System.out.println(Thread.currentThread().getName() + ": num = " + num);
                }
            }
        }.start();

        //输出
        Thread-0: num = 1000
        Thread-3: num = 2000
        Thread-1: num = 3000
        Thread-4: num = 4000
        Thread-6: num = 5000
        Thread-2: num = 6000
        Thread-7: num = 7000
        Thread-8: num = 8000
        Thread-5: num = 9000
        Thread-9: num = 10000
        main, num = 10000

这就能正确的同步,synchronized实际上是一种锁机制,括号里面是锁的内容,这里锁的是当前类的.class对象,对象在当前进程中是单例的,只有一个。当一个线程获取到该锁后,其他线程再去尝试获取锁就会等待,直到持有锁的线程运行完毕,自动释放锁后,再去尝试获取该锁。即由synchronized保护的代码块每次只能由一个线程运行,这样就保证了同步性。synchronized可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。

线程启动相关

在Java中,我们可以利用Thread的子类启动线程,也可以实现Runnable的接口来启动线程;Thread本质也是实现了Runnable;

public class MyThread extends Thread{
   @Override
   public void run(){
       //TODO
   }
}
new MyThread().start();
public class MyRunnable implements Runnable{
   @Override
   public void run(){
       //TODO
   }
}
new MyRunnable().start();

当然,还可以用ThreadFactory来启动线程

ThreadFactory factory = Executors.defaultThreadFactory();
factory.newThread(new Runnable() {
    @Override
     public void run() {
         //TODO
     }
}).start();

还有一个带返回的线程Callable+FutureTask

Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        return new Random().nextInt(10);
    }
};
FutureTask<Integer> future = new FutureTask<Integer>(callable);
new Thread(future).start();
//获取返回值
try {
    System.out.println(future.get());
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

注意启动线程的操作是start()方法,而不是run()方法,以run()启动的线程实际上是串行执行的代码,即直接执行线程对象的run()方法,而不是启动一个线程

总结

我们可以看出volatile虽然具有可见性但是并不能保证原子性。

性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。

但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。

线程其他知识

线程暂停

线程有一个sleep的静态方法用于暂停线程,并且会阻塞,不会释放已经持有的锁。单位:毫秒

Thread.sleep(1000);

线程等待

等待队列

所有的实例对象都有一个等待队列,它是在实例的wait方法执行后停止操作的线程的队列。在执行完wait方法后,线程便会暂停操作,进入等待队列。除非发生以下其中一种情况,否则将一直在等待队列中休眠。反之将会退出队列。等待队列是一个虚拟的概念,它既不是实例中的字段,也不是用于获取正在实例上等待的线程列表的方法。
· 有其他线程的notify方法来唤醒线程
· 有其他线程的notifyAll方法来唤醒线程
· 有其他线程的interrupt方法来唤醒线程
· wait方法超时

将线程放入等待队列

Object obj =new Object();
new Thread(){
    @Override
    public void run(){
        synchronized(obj){
            .....
            obj.wait();//线程将进入等待队列
            .....
        }
    }
}.start();

假如是锁当前对象,则wait()等同于this.wait()。

public clsss TestWait{
    public static void main(String[] args){
        TestWait test = new TestWait();
        new Thread(){
            @Override
            public void run(){
                test.testWait();
            }
        }.start();
    }
    public void testWait(){
        synchronized(TestWait.this){
            try {
                wait();//等同于this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

注意执行wait方法必须持有锁,线程进入等待队列必须释放锁,这也是跟sleep不同的地方,sleep会阻塞不会释放锁。

wait.png

从等待队列中取出线程

notify方法会将等待队列中的一个线程取出。

public class TestWait {
    public static void main(String[] args) {
        TestWait test = new TestWait();
        new Thread() {
            @Override
            public void run() {
                test.testWait();
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test.testNotify();
            }
        }.start();
    }

    public void testWait() {
        synchronized (TestWait.this) {
            try {
                System.out.println("进入等待队列" + System.currentTimeMillis() / 1000);
                wait();//等同于this.wait();
                System.out.println("从等待队列中恢复执行" + System.currentTimeMillis() / 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void testNotify() {
        synchronized (TestWait.this) {
            notify();//等同于this.notify();
        }
    }
}
//输出
进入等待队列1524497926
从等待队列中恢复执行1524497927

那么第一个线程确实被唤醒了,并且时间差一秒。

notify.png

注意B执行notify后A不会立即运行,而是要等B运行完后释放锁,A这时候去重新获取锁后,才能运行。

取出等待队列的所有线程

notify只能唤醒一个线程,notify会唤醒所有在等待队列中的线程。其他跟notify一样。注意notify,notifyAll,wait均需要获取锁才能使用,否则会抛出异常java.lang. IllegalMonitorStateException
由于使用notify只能唤醒一个线程,所以处理速度比notifyAll快;但使用notify时,如果处理不好,程序边可能终止。一般来说,使用notifyAll的代码比notify要更为健壮

区别

wait,notify,notifyAll是Object类的方法,而不是Thread类的固有方法;sleep是Thread类的静态方法。但由于Oeject是所有类的父类,所以wait,notify,notifyAll也可以通过Thread使用,但不建议。

线程状态切换图

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

推荐阅读更多精彩内容