1.2使用多线程

一个进程正在运行时,至少会有一个线程在运行。线程在后台默默执行,比如调用main方法的线程就是如此,它是由JVM创建的。

class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
    }
}

程序执行后显示:

这个main是一个叫做main的线程在执行main()方法中的代号。main与main()方法没有什么关系,仅仅是名字相同而已。

1.2.1继承Thread类:

在java中实现多线程编程的方式主要有两种:一种是继承Thread类,另一种是实现Runnable接口。

注源码中:

public class Thread implements Runnable

Thread与Runnable是具有多态关系的。

通过继承Thread实现多线程的最大问题是无法继承其他类(因为java中是单根继承的),所以要想支持多继承,可以实现Runnable接口的同时继承其他类。

不论是继承Thread还是实现Runnable接口,创建的线程都是工作时的性质都是一样的。

继承Thread实现:

public class TestThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("这是测试线程");
    }
}

 main方法中:

public class Main {
    public static void main(String[] args) {
        TestThread tt  = new TestThread();
        tt.run();
        System.out.println("运行结束");
    }
}

结果如图:

线程是一个子任务,CPU以不确定的方式,或者说以随机的时间来调用线程中的run()方法,所以才会先打印"运行结束"后打印"这是测试线程了"。

注:也许是cpu进步了?用的i7-7700

实际上这里测试的数据是正确的,反而书中给的结果有问题:在main()中调用的是run()方法,这里会将线程中的run()交给当前线程执行,也就是交给了mian()所在的线程执行,此时在mian()中代码就是顺序执行的,也就是说说测试的数据结果不会变,永远都是这个顺序。

如果多次调用start()方法,则会出现异常:

上述表现了线程的随机性。

下面的案例将演示线程的随机性

继承Thread实现:

public class TestThread extends Thread{
    @Override
    public void run() {
        super.run();
        try {
            for (int i = 0; i < 10; i++) {
                int time = (int)(Math.random() * 1000);
                Thread.sleep(time);
                System.out.println("run=" + Thread.currentThread().getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行在main()的代码:

public class Main {
    public static void main(String[] args) {
        try {
            TestThread tt = new TestThread();
            tt.setName("TestThread");
            tt.start();
            for (int i = 0; i < 10; i++) {
                int time = (int)(Math.random() * 1000);
                Thread.sleep(time);
                System.out.println("main=" + Thread.currentThread().getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

结果:

在代码中使用随机数的形式,使线程得到挂起的效果,从而表现cpu执行线程时具有不确定性。

Thread.start()方法是告诉“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法,这个过程就是让系统安排一个时间来调用Thread中的run()方法。从而使线程运行,启动线程,具有异步的效果。

注:如果调用Thread.run()方法,就是同步的了,因为此时线程对象会交给main线程来处理,此时在main()中执行过程为线性的。

执行start()方法的顺序不代表线程执行的顺序。(线程的执行在定义时就讲过了,是在线程间进行切换的,以为切换速度快,看起来是同一时间完成了多件事。)

1.2.2实现Runnable接口:

public class TestRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程运行中");
    }
}

通过Thread的构造方法使用TestRunnable测试类:

运行代码:

public class Main {
    public static void main(String[] args) {
        TestRunnable tr = new TestRunnable();
        Thread t = new Thread(tr);
        t.start();
        System.out.println("运行结束");
    }
}

 运行结果:

注:Thread实现了Runnable,所以构造方法Thread(Runnable target)不光可以传入一个Runnable接口对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()交由其他线程完成。

1.2.3实例变量和线程安全:

自定义线程中的实例变量针对其他线程可以分为共享和不共享之分,这在多个线程间进行交互时是个很重要的技术点。

(1)不共享的情况:

线程代码:

public class TestShare extends Thread {
    private int count = 5;
    public TestShare(String name) {
        super();
        this.setName(name);//设置线程名称。
    }
    @Override
    public void run() {
        super.run();
        while(count > 0) {
            count--;
            System.out.println("由" + currentThread().getName() + "计算 + count=" +count);
        }
    }
}

执行代码:

public class Main {
    public static void main(String[] args) {
        TestShare ts1 = new TestShare("A");
        TestShare ts2 = new TestShare("B");
        TestShare ts3 = new TestShare("C");
        ts1.start();
        ts2.start();
        ts3.start();
    }
}

执行结果:

每个线程都有各自的count数,都是从5开始计数,各自减少各自的,这种情况就是变量不共享。此例中不存在多个线程访问同一个实例变量的情况。

(2)共享的情况

:这种情况就有很多现实中的模型了:抢票,抢购,秒杀等等。

 线程代码:

public class TestShare1 extends Thread {
    private int count = 5;

    @Override
    public void run() {
        super.run();
        count--;
        //此示例不要用for语句,因为使用同步后其他线程就得不到运行的机会了。
        //一直由一个线程进行减法运算。
        System.out.println("由" + Thread.currentThread().getName() + "计算,count=" + count);
    }
}

执行代码:

public class Main {
    public static void main(String[] args) {
        TestShare1 ts1 = new TestShare1();
        Thread A = new Thread(ts1,"A");
        Thread B = new Thread(ts1,"B");
        Thread C = new Thread(ts1,"C");
        Thread D = new Thread(ts1,"D");
        Thread E = new Thread(ts1,"E");
        A.start();
        B.start();
        C.start();
        D.start();
        E.start();
    }
}

 执行结果:

B与A都是3,说明A与B同时对共享资源做了处理,这是非线程安全得,这里就产生了线程安全性问题。

原因:

在某些JVM中,i--的操作要经历下面三个步骤:

  1. 取得原有i值
  2. 计算i-1
  3. 对i进行赋值

在这3个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。

为了解决这个问题,需要在每次执行run()时进行同步(加锁,只有当run()执行完毕才会切换线程)。

线程代码:

public class TestShare2 extends Thread {
    private int count = 5;

    @Override
    synchronized public void run() {
        super.run();
        count--;
        System.out.println("由"+ currentThread().getName() + "计算,count=" +count);
    }
}

运行结果:

run()方法前加synchronized关键字,为线程加锁,当第一个线程运行到此处时,会进行加锁,在运行完之前不会放开锁,此时线程被切换其他线程运行到此时,就会进行排队,等到其他线程运行完run()才能够进入方法并运行。

注:synchronized关键字可以在任意对象及方法上加锁,而这种加锁的代码成为:“互斥区”或“临界区”。

注:术语:“非线程安全”。非线程安全是指:多个线程对同一个对象中的同一个实例变量进行操作时出现值被更改、值不同步的情况,进而影响程序的执行流程。

解决非线程安全示例:

模拟Servlet组件:

public class LoginServlet {
    private static String usernameRef;
    private static String passwordRef;

    public static void doPost(String username, String password) {
        try {
            usernameRef = username;
            if ("a".equals(usernameRef)) {
                Thread.sleep(500);
            }
            passwordRef = password;
            System.out.println("username=" + usernameRef +  "    password=" + password);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 线程A:

public class ALogin extends Thread {
    @Override
    public void run() {
        super.run();
        LoginServlet.doPost("a","aa");
    }
}

 线程B:

public class BLogin extends Thread {
    @Override
    public void run() {
        LoginServlet.doPost("b", "bb");
    }
}

 执行代码:

public class Main {
    public static void main(String[] args) {
        ALogin a = new ALogin();
        a.start();
        BLogin b = new BLogin();
        b.start();
    }
}

 执行结果:

 解决线程不安全的问题代码:

public class LoginServlet {
    private static String usernameRef;
    private static String passwordRef;

    synchronized public static void doPost(String username, String password) {
        try {
            usernameRef = username;
            if ("a".equals(usernameRef)) {
                Thread.sleep(500);
            }
            passwordRef = password;
            System.out.println("username=" + usernameRef +  "    password=" + password);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 执行结果:

1.2.4留意i--与System.out.println()的异常

前面章节通过synchronized解决了非线程安全问题。

本节将细化println()方法与i++联合使用时“有可能”出现的另外一种异常情况。并说明其中原因。

线程代码:

public class FourThread extends Thread {
    private int i = 5;

    @Override
    public void run() {
        System.out.println("i=" + (i--) + " threadName= " + Thread.currentThread().getName());
    }
}

 执行代码:

public class Main {
    public static void main(String[] args) {
        FourThread ft = new FourThread();
        Thread t1 = new Thread(ft);
        Thread t2 = new Thread(ft);
        Thread t3 = new Thread(ft);
        Thread t4 = new Thread(ft);
        Thread t5 = new Thread(ft);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

 结果:

原因:

虽然println()方法在内部时同步的,但i--的操作却是在进入println()前发生的,所以有发生非线程安全问题的概率。

所以为了防止发生非线程安全问题,还是应该继续使用同步方法。

 源码地址:https://github.com/lilinzhiyu/threadLearning

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏云霄雨霁

Java虚拟机--Class文件结构

1615
来自专栏java学习

Java基础总结大全(1)

一、基础知识: 1、JVM、JRE和JDK的区别: JVM(Java Virtual Machine):java虚拟机,用于保证java的跨平台的特性。 ...

36711
来自专栏zhangdd.com

nginx location匹配规则

~      #波浪线表示执行一个正则匹配,区分大小写 ~*    #表示执行一个正则匹配,不区分大小写 ^~    #^~表示普通字符匹配,如果该选项匹配...

954
来自专栏Deep learning进阶路

C++随记(八)---存储持续性、作用域和链接性

版权声明:本篇文章是阅读《C++primer plus (第6版)中文版》第9章之后所作的笔记。部分文字和图表摘自于这本书。 C++随记(八)---存储持续性、...

1880
来自专栏决胜机器学习

设计模式专题(十六)——迭代器模式

设计模式专题(十六)——迭代器模式 (原创内容,转载请注明来源,谢谢) 一、概述 迭代器模式(Iterator)提供一种方法,顺序访问一个聚合对象中的每个元素...

3538
来自专栏LEo的网络日志

python技巧分享(七)

3308
来自专栏PHP在线

php

单例设计模式 1.控制一个类只能创建一个对象,设置构造函数为私有的。 2.设置静态方法调用类中方法返回实例化。 3.在类中设置静态属性存放实例化对象。 ? 命名...

3247
来自专栏desperate633

深入理解Java中的内存泄漏内存泄漏内存泄漏发生的原因造成内存泄露的常见情形内存泄露的解决方案

Java的一个最显著的优势是内存管理。你只需要简单的创建对象而不需要负责释放空间,因为Java的垃圾回收器会负责内存的回收。然而,情况并不是这样简单,内存泄露还...

931
来自专栏Linyb极客之路

并发编程之Synchronized关键字

一、Synchronized的基本使用 Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作...

2796
来自专栏吴裕超

exports 和 module.exports 的区别

require 用来加载代码,而 exports 和 module.exports 则用来导出代码。但很多新手可能会迷惑于 exports 和 module.e...

2714

扫码关注云+社区