首页 > 代码库 > java基础回顾(五)线程详解以及synchronized关键字

java基础回顾(五)线程详解以及synchronized关键字

本文将从线程的使用方式、源码、synchronized关键字的使用方式和陷阱以及一些例子展开java线程和synchronized关键字的内容。

一、线程的概念

线程就是程序中单独顺序的流控制。线程本 身不能运行,它只能用于程序中。 

 二、线程的实现

线程的实现有两种方式:

1.继承Thread类并重写run方法

2.通过定义实现Runnable接口的类进而实现run方法

当用第一种方式时我们需要重写run方法因为Thread类里的run方法什么也不做(见下边的源码),当用第二种方式时我们需要实现Runnable接口的run方法,然后使用new Thread(new Runnable())来生成线程对象,这时的线程对象的run方法就会调用Runnable的run方法,这样我们自己编写的run方法就执行了。

技术分享

将我们希望线程执行的代码放在run方法中,然后通过start方法来启动线程,start方法首先为线程的执行准备好系统资源,然后再去调用run方法。下边例子中将会有代码。

在jdk源码里start方法里面调用了start0()方法,他是一个native方法,我们不可见。线程一经运行就不会受我们的控制,Thtead类里其实有stop方法,但是不能通过挑用stop()方法,而是应该让run方法自然结束。

有一些需要注意的地方用代码举例说明:

例一:

public class ThreadTest2 {
    public static void main(String[] args) {
//        Thread thread = new Thread(new Runnable() {
//            @Override
//            public void run() {
//                for(int i = 0; i < 100; i++) {
//                    System.out.println("hello" + i);
//                }
//            }
//        });
        
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
    
        thread.start();
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i < 100; i++) {
            System.out.println("hello" + i);
        }
    }
}

上面的代码是采用第二种方式通过定义实现Runnable接口的类进而实现run方法,重点看注释掉的部分当生命一个简单的Thread类时用匿名内部类来实现是一个比较好的方式。

例二:

public class ThreadTest3 {
    public static void main(String[] args) {
        Runnable r = new Thread3();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        
        t1.start();
        t2.start();
    }
}

class Thread3 implements Runnable {

    int i;
    
    @Override
    public void run() {
        
//        i是成员变量和局部变量的结果是不一样的
//        对于i是成员变量的时候,所有线程共享这一个成员变量,不管有几个线程只要i加到了10就会终止线程,所以最后的结果一定只有10个。
//        对于i是一个局部变量时,每个线程都会有一份局部变量的拷贝,并且线程与线程之间是互不影响的
//        int i = 0;
        
        while(true) {
            System.out.println("number = " + i++);
            
            try {
                Thread.sleep((long)(Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            if(10 == i) {
                break;
            }
        }
    }
}

关于成员变量与局部变量:如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作时,他们对成员变量是彼此影响的(也就是说一个线程对成员变量的海边会影响到另一个线程)。如果一个变量是局部变量那么每个线程都会有一个该局部变量的拷贝,一个线程对局部变量的改变是不会影响到其他线程的。

例三:

public class ThreadTest4 {
    public static void main(String[] args) {
        Runnable r = new Thread4();
        Thread t1 = new Thread(r);
        
//        重新声明一个对象的话,就又会打印20个结果。这是因为不是原来的对象了相对应的成员变量是分别在两个对象里的,所以当然也是不会相互影响的。所以会打印20个结果。
//        r = new Thread4();
        Thread t2 = new Thread(r);
        
        t1.start();
        t2.start();
    }
}

class Thread4 implements Runnable {

    int i;
    
    @Override
    public void run() {
        
        while(true) {
            System.out.println("number = " + i++);
            
            try {
                Thread.sleep((long)(Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            if(10 == i) {
                break;
            }
        }
    }
}

结果是输出number = 0到number = 9而且只顺序输出一遍,但是如果没有注释掉r = new Thread4();的话程序就会交替输出两遍number = 0到number = 9。其实就是因为重新声明了对象两个线程作用的成员变量不是一个,从而会输出两次。

三、synchronized关键字(重点介绍)

为什么要引入同步机制?

在多线程环境中,可能会有两个甚至多个线程试图同时访问一个有限的资源。这时就会发生许多意想不到的资源冲突,必须对这种冲突进行预防由此引入同步机制。

synchronize关键字简介:当synchronize关键字修饰一个方法时,该方法叫做同步方法。java中的每个对象都有一个锁(或者叫做监视器),当访问某个对象的synchronize方法时,表示将该对象上锁(不是方法),此时其他任何线程都无法再去访问该方法了,直到之前的那个线程执行方法完毕后或者是抛出了异常,那么将该对象的锁释放掉,其他线程才能再去访问该synchronize方法。这样就实现了方法的单线程访问。

synchronized关键字有两种使用方法:
  1. 在方法前用synchronized关键字修饰。
  2. 使用synchronized代码块。
synchronized方法是一种粗粒度的并发控制,某一时刻只能有一个线程执行该synchronized方法,synchronized块是一种细粒度的并发控制,只会将代码块中的代码同步,位于方法内,synchronized块之外的方法里的代码是可以被多个线程同时访问到的。

接下来我们用实例来看一看synchronized关键字的特性以及工作方式

例一:

public class ThreadTest5 {
    public static void main(String[] args) {
        Example example = new Example();
        Thread t1 = new TheThread(example);//        example = new Example(); //加上这行代码的话,两个线程就会交替执行,说明synchronize关键字是作用于对象层面上的。
        Thread t2 = new TheThread2(example);

        t1.start();
        t2.start();
    }
}

class Example {
    
    public synchronized void execute() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("hello: " + i);
        }
    }

    public synchronized void execute2() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("world: " + i);
        }
    }
}

class TheThread extends Thread {
    private Example example;

    public TheThread(Example example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute();
    }
}

class TheThread2 extends Thread {
    private Example example;

    public TheThread2(Example example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute2();
    }
}

执行结果先顺序输出hello: 0到hello: 19 再顺序输出world: 0到world: 19

需要注意的是如果重新生成一个Example对象的话就不是顺序输出了而是交替不规则的输出了:

技术分享

只截取了部分数据,之所以交替输出是因为synchronized关键字是作用于对象上的,两个线程调用两个不同的对象的synchronized方法不会发生抢占,所以两个线程同时进行。只有一个对象的时候,如果一个对象有多个synchronize方法,某一个时刻某个线程已经进入到了某个synchronize方法,那么在该方法没有执行完毕前,其他线程是无法访问到该对象的任何synchronize方法的。所以是顺序输出。

例二:

public class ThreadTest6 {
    public static void main(String[] args) {
        Example2 example = new Example2();
        Thread t1 = new TheThread3(example);
        example = new Example2();
        Thread t2 = new TheThread4(example);

        t1.start();
        t2.start();
    }
}

class Example2 {
//    synchronized修饰的方法如果是静态方法那么synchronized就不是作用于方法所在的对象了,而是方法所在对象的class对象。也就是类本身,对象有多个但是对象所对应的的class对象肯定是只有一个
    public synchronized static void execute() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("hello: " + i);
        }
    }

    public synchronized static void execute2() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("world: " + i);
        }
    }
}

class TheThread3 extends Thread {
    private Example2 example;

    public TheThread3(Example2 example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute();
    }
}

class TheThread4 extends Thread {
    private Example2 example;

    public TheThread4(Example2 example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute2();
    }
}

例二的执行结果是 先顺序输出hello: 0到hello: 19 再顺序输出world: 0到world: 19

 这时候就奇怪了,例二和例一(没有注释第五行代码的情况下)的区别只有static修饰的区别怎么结果完全不一样。

如果某个synchronize方法是static的,那么当线程访问该方法时,他的锁并不是synchronize方法所在的对象,而是synchronize方法所在的对象所对应的的class对象,因为java中无论一个类有多少个对象,这些对象只会唯一对应一个class对象,因此当线程分别访问同一个类的两个对象的两个static synchronized方法时,他们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始执行。

例三:

public class ThreadTest7 {
    public static void main(String[] args) {
        Example3 e = new Example3();
        TheThread5 t1 = new TheThread5(e);
        // e = new Example3();
        TheThread6 t2 = new TheThread6(e);

        t1.start();
        t2.start();
    }
}

class Example3 {
    // 没有实际意义,任何一个对象都行,不写也没事用this就好。
    private Object object = new Object();

    public void execute() {
        // synchronized代码块
        synchronized (this) {
            for (int i = 0; i < 20; i++) {
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("hello: " + i);
            }
        }

    }

    public void execute2() {
        synchronized (this) {
            for (int i = 0; i < 20; i++) {
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("world: " + i);
            }
        }

    }
}

class TheThread5 extends Thread {
    private Example3 example;

    public TheThread5(Example3 example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute();
    }
}

class TheThread6 extends Thread {
    private Example3 example;

    public TheThread6(Example3 example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute2();
    }
}

这个例子主要说明的是synchronized关键字第二种用法:synchronized代码块。

上面已经介绍过了synchronized代码块是一种细粒度的并发控制,只会将代码块中的代码同步,位于方法内,synchronized块之外的方法里的代码是可以被多个线程同时访问到的。相对于synchronized代码块来说synchronized方法是一种更加重量级的并发控制机制。

 

好啦今天就这些吧,洗洗睡了。。。

 

java基础回顾(五)线程详解以及synchronized关键字