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

前言

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-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-1
Thread-1
Thread-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...10
Thread-0...10
Thread-2...10
Thread-1...10
Thread-2...7
Thread-0...8
Thread-3...9
Thread-0...4
Thread-2...5
Thread-1...6
Thread-2...1
Thread-0...2
Thread-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”锁,而代码块的锁对象可以任意,如果线程任务只需要一个同步时可用同步函数,如果需要多个同步时,必须使用不同的锁来区分。

总结

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

你可能感兴趣的:(java,多线程,Thread,Runnable,实现,Java)