java多线程(1):线程的创建和多线程的安全问题

来源:互联网 发布:vps建站 编辑:程序博客网 时间:2024/06/06 03:06

前言

java多线程多用于服务端的高并发编程,本文就java线程的创建和多线程安全问题进行讨论。

正文

一,创建java线程

创建java线程有2种方式,一种是继承自Thread类,另一种是实现Runnable接口。由于java只支持单继承,所以很多时候继承也是一种很宝贵的资源,我们多采用继承Runnable接口的方式。下面来看一下这两种方式。

1,继承Thread,其中包括关键的4步

package com.jimmy.basic;class MyThread extends Thread{  // 1,继承Thread    public void run() {         // 2,重写run()方法        for (int i = 0; i < 10; i++)         {            System.out.println(Thread.currentThread().getName());        }    }}public class ExtendsThread {    public static void main(String[] args) {        MyThread myThread1 = new MyThread();  // 3,创建线程实例        MyThread myThread2 = new MyThread();        myThread2.start();                    // 4,start()方法启动线程        myThread1.start();          }}

多线程执行的代码都写在run()方法体里面。上面代码run方法中表示循环输出10次线程的名字。测试代码中创建2个线程并启动,那么这两个线程交替执行各自run方法中的代码,共产生20条输出记录。上面这段代码的输出如下:

Thread-1Thread-1Thread-1Thread-1Thread-1Thread-1Thread-1Thread-0Thread-0Thread-0Thread-0Thread-0Thread-0Thread-0Thread-0Thread-0Thread-0Thread-1Thread-1Thread-1

2,实现Runnable接口

package com.jimmy.basic;class MyThread2 implements Runnable{   // 1,类实现Runnable接口    @Override    public void run() {                // 2,实现run方法        for (int i = 0; i < 10; i++) {            System.out.println(Thread.currentThread().getName());        }    }}public class ImplementsRunnable {    public static void main(String[] args) {        MyThread2 mt = new MyThread2(); //  3,实例化接口Runnable子类对象        Thread thread1 = new Thread(mt);//  4,将Runnable子类对象传递给Thread类的构造函数        Thread thread2 = new Thread(mt);        thread1.start();// 5,开启线程        thread2.start();    }}

实现接口是我们推荐的创建线程的方法。Runnable接口中只有一个run方法,我们在创建Thread线程对象时,将实现了Runnable接口的子类对象传递给Thread的构造函数:Thread(Runnable target)。此时再使用start()方法开启线程时,就会执行Runnable接口的子类中的run方法。我们看下Thread的源码

//Thread类的部分源码class Thread implements Runnable {    private Runnable target;  // 持有Runnable类型变量    public Thread(Runnable target) {  // 构造函数,构造过程借助于init函数        init(null, target, "Thread-" + nextThreadNum(), 0);    }    private void init(ThreadGroup g, Runnable target, String name,                      long stackSize) {        init(g, target, name, stackSize, null);    }    private void init(ThreadGroup g, Runnable target, String name,                      long stackSize, AccessControlContext acc) {        //这里略去了函数其他传来的参数的操作         this.target = target;    }    public synchronized void start() {        //这里略去了其他初始化步骤        run();    }    @Override    public void run() {          if (target != null) {  // 如果有Runnable接口子类对象传进来,就执行其run方法。不然什么都不做。            target.run();        }    }}

截取了Thread类源码中的一部分。可以看出,Thread类中持有一个Runnable接口类型变量,并提供该接口变量的构造函数,虽然其构造过程放到了init()方法中了,一样的。重点是Thread类的run方法会判断创建线程的时候是否传入了Runnable子类对象,如果有,就执行Runnable子类对象的run()方法。

所以Runnable接口在创建线程时,跟前面直接继承Thread类不同。要先实例化Runnable子类对象,然后在创建Thread类时,将其作为参数传递给Thread类的构造函数。其运行结果跟前面类似,20条记录交替执行。

二,多线程的安全问题

我们看到,前面的代码中,每个线程在各自的栈内存中交替执行,互不影响。之所以互不影响,是因为run方法中代码没有操作线程共享的变量。一旦各个线程都要操作共享变量,那么就可能会出现线程安全问题。下面来看一个小例子,这个例子中4个线程操作同一个共享变量。我们来看一下会出现什么问题,以及怎么解决。

package com.jimmy.basic;class SellTickets implements Runnable {    private int tickets = 10;  // 共享变量    @Override    public void run() {   // 实现run方法        sell();      // 调用sell方法    }    public void sell(){        while (tickets > 0) {            System.out.println(Thread.currentThread().getName() + "..." + tickets);            tickets--;        }    }}public class TicketsSharedVariable {    public static void main(String[] args) {        SellTickets sellTickets = new SellTickets(); // 实例化接口对象        Thread thread1 = new Thread(sellTickets); // 只有传入Runnable子类对象的Thread才能共享变量        Thread thread2 = new Thread(sellTickets);        Thread thread3 = new Thread(sellTickets);        Thread thread4 = new Thread(sellTickets);        thread4.start();  // 启动线程        thread3.start();        thread2.start();        thread1.start();    }}

注意,tickets变量定义在Runnable接口子类中,并不是我们说它是共享变量,它就是共享变量。而是将Runnable子类对象传递给Thread的构造函数,传递后的线程才能共享这个tickets变量。

像下面这样就不会是共享变量,而是各个线程的私有变量。

Thread thread1 = new SellTickets2();  // 不传参而创建的线程,每一个都有自己的变量Thread thread2 = new SellTickets2();Thread thread3 = new SellTickets2();Thread thread4 = new SellTickets2();

当线程操作共享变量时,问题就出现了。下面是上面代码的输出。

Thread-3...10Thread-0...10Thread-2...10Thread-1...10Thread-2...7Thread-0...8Thread-3...9Thread-0...4Thread-2...5Thread-1...6Thread-2...1Thread-0...2Thread-3...3

从输出上来看,很明显出现了线程安全的问题,这样的操作显然是不正确的。究其原因,是各个线程在进行sell方法操作时,抢占了执行顺序。我们希望一个线程在操作变量的时候,不会被其他线程干扰。也就是说,如果一个线程在执行sell方法的时候具有原子性,也就是不能有其他线程再来执行sell方法。

java保证操作的原子性很简单,就是synchronized关键字。该关键字既可以用来修饰代码块,也可以用来修饰函数。synchronized可以理解为加锁,为代码块加锁,为函数加锁。既然是加锁,那么锁怎么来表示呢?“锁”也是对象,在代码块上使用要显示加锁,如下:

Object obj = new Object();public void sell(){        synchronized (obj) { // 锁对象可以是任意对象            while (true) {                if (tickets > 0) {                    System.out.println(Thread.currentThread().getName() + "..." + tickets);                    tickets--;                }            }        }           }

上面就是同步代码块的使用,将需要同步的代码放进同步代码块,就可以实现线程同步。既然是对需要同步的代码进行封装,就可以将synchronized用在函数上,用法如下:

public synchronized void sell() {  // synchronized修饰函数,使用的是this锁对象。        while (true) {            if (tickets > 0) {                System.out.println(Thread.currentThread().getName() + "..." + tickets);                tickets--;            }        }    }

一般都会使用函数来封装同步代码,再用synchronized来修饰函数,实现线程同步。注:static静态函数使用的是“类名.class”锁对象。

最后说一下同步代码块和同步函数的区别。函数使用固定的“this”锁,而代码块的锁对象可以任意,如果线程任务只需要一个同步时可用同步函数,如果需要多个同步时,必须使用不同的锁来区分。

总结

线程的安全需要同步来实现。

0 0
原创粉丝点击