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 条评论
登录 后参与评论

相关文章

来自专栏mini188

java中的锁

java中有哪些锁 这个问题在我看了一遍<java并发编程>后尽然无法回答,说明自己对于锁的概念了解的不够。于是再次翻看了一下书里的内容,突然有点打开脑门的感觉...

1869
来自专栏java工会

Java多线程实现的三种方式

1505
来自专栏Java帮帮-微信公众号-技术文章全总结

第二十五天 多线程-常用方法&线程池【悟空教程】

System.out.println(getName() + " ==== " + i );

1063
来自专栏Java学习网

Java 实现线程死锁

Java 实现线程死锁 概述 春节的时候去面试了一家公司,笔试题里面有一道是使用简单的代码实现线程的‘死锁’,当时没有想到这道题考的是Synchronized关...

2266
来自专栏微信公众号:Java团长

Java多线程实现的三种方式

Java多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果...

1023
来自专栏Java进阶之路

使用CompletionService非阻塞获取多线程返回值

Future接口是Java标准API的一部分,在java.util.concurrent包中。Future接口是Java线程Future模式的实现,可以来进行异...

472
来自专栏Java技术栈

多线程 start 和 run 方法到底有什么区别?

昨天栈长介绍了《Java多线程可以分组,还能这样玩!》线程分组的妙用。今天,栈长会详细介绍 Java 中的多线程 start() 和 run() 两个方法,Ja...

301
来自专栏好好学java的技术栈

java并发基础篇(五): 创建线程的四种方式

在详细了解这四种方法之前,先来理解一下为什么线程要这样创建:形象点来说,Thread是一个工人,run()方法里面的便是他的任务栏,这个任务栏默认是空的。当你想...

401
来自专栏青枫的专栏

java基础学习_多线程02_多线程、设计模式_day24总结

601
来自专栏微信公众号:Java团长

Java多线程编程

1)设计更复杂 虽然有一些多线程应用程序比单线程的应用程序要简单,但其他的一般都更复杂。在多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往往...

1292

扫码关注云+社区