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

相关文章

来自专栏老马说编程

(72) 显式条件 / 计算机程序的思维逻辑

上节我们介绍了显式锁,本节介绍关联的显式条件,介绍其用法和原理。显式条件也可以被称做条件变量、条件队列、或条件,后文我们可能会交替使用。 用法 基本概念和方法...

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

Java并发学习2【面试+工作】

  关键字synchronized的作用是实现进程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性(即同步块每...

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

Java多线程编程

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

1252
来自专栏北京马哥教育

Python多进程编程

阅读目录 1. Process 2. Lock 3. Semaphore 4. Event 5. Queue 6. Pipe 7. Pool 序. multi...

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

03.线程安全/同步/线程通讯

03.线程安全/同步/线程通讯 一.一个典型的Java线程安全例子 ? ? 上面例子很容易理解,有一张银行卡,里面有1000的余额,程序模拟你和你老婆同时在取款...

3357
来自专栏向治洪

多线程之传统多线程

Contents 传统线程技术 传统创建线程方式 传统定时器技术 互斥 同步 传统线程技术 传统创建线程方式 1.继承Thread类,覆盖run方法 ...

1819
来自专栏青枫的专栏

java基础学习_多线程01_多线程_day23总结

562
来自专栏noteless

-1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),

java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁  sleep()和wait()方法...

724
来自专栏zingpLiu

浅析Python多线程

学习Python多线程的资料很多,吐槽Python多线程的博客也不少。本文主要介绍Python多线程实际应用,且假设读者已经了解多线程的基本概念。如果读者对进程...

887
来自专栏spring源码深度学习

java基础thread——java5之后的多线程(浅尝辄止)

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个...

681

扫码关注云+社区