[多线程]Java多线程03_线程同步及可见性

[多线程]Java多线程03_线程同步及可见性

一、线程安全问题

线程安全问题是个经典的问题,主要涉及的方面就是对变量的修改以及读取。因为修改读取并不是一步完成,而计算机在切换线程的时候也没有清楚哪个时候可以切换,这时候就需要程序来给计算机指定道路说,你什么时候可以切换什么时候不行。特别是在对值修改的过程中,是不能够切换到读取线程的,要不然会出现脏读(即数据的错误)的问题。而Java对线程安全的控制主要是synchronized以及Lock锁。

关于变量安全控制的问题,我们现在大部分说到的变量安全问题均是类全局变量的安全问题,方法内部的局部变量,因为jvm在执行一个方法的时候都会进行线程栈之间的隔离,所以一个线程中对一个变量的修改创建并不会到另外一个线程对变量的修改创建。

(一)synchronized关键字

synchronized关键字主要用来修饰方法以及代码块,细节待会再说。被修饰的方法或者代码块只要有一个线程在执行,其他线程则必须等待该线程执行完毕,这就能够保证在赋值的时候,是赋值完成才切换给读取值的线程使用。并且可以通过Object类的wait()以及notify()方法进行线程之间的协调工作。但是细粒度并没有那么大,这也就是Lock类出现的理由。

(二)Lock锁

Lock值更能够细粒度的控制什么时候需要加锁,什么时候可以解锁,并且在线程之间通讯方面更加成熟,可以实现多个线程之间,指定线程的通讯。Lock锁放在后面才会说到

二、synchronized关键字

(一)基础用法和区别

示例代码

示例中一共有三个类:
– 图书类:代表图书,有一个方法setBookNameAndCode()是用来赋值的,也就是这个方法来测试线程安不安全
– 操作图书的业务层:用于创建不同的业务层给一个图书赋值
– 线程启动类:启动两个不同的业务层线程给一个共同的图书赋值

/**
 * 图书类
 * @author liweidan
 */
public class Book {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public synchronized void setBookNameAndCode(String name, Integer code) {
        try {
            this.name = name;
            Thread.sleep(2000L);
            this.bookCode = code;
            System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    /** 省略getter setter toString */
}

/**
 * 图书业务类
 * @author liweidan
 * @date 2017.12.15 下午3:48
 * @email toweidan@126.com
 */
public class BookService implements Runnable {
    Book book;
    String name;
    Integer code;
    public BookService(Book book, String name, Integer code) {
        this.book = book;
        this.name = name;
        this.code = code;
    }
    @Override
    public void run() {
        book.setBookNameAndCode(name, code);
    }
}

public static void testSync01() {
    /** 创建一个共享资源 */
    Book book = new Book();
    /** 创建两个线程来对这个共享资源进行修改 */
    Thread t1 = new Thread(new BookService(book, "Java多线程", 10001), "Thread01");
    Thread t2 = new Thread(new BookService(book, "JVM虚拟机", 10002), "Thread02");
    t1.start();
    t2.start();

}

结果:

一共有两个结果:

    /**
     没有加synchronized:
        bookName: JVM虚拟机, code: 10001
        bookName: JVM虚拟机, code: 10001
     加synchronized:
         bookName: Java多线程, code: 10001
         bookName: JVM虚拟机, code: 10002
     */

1.没有加synchronized的情况

$seq
Thread01->共同的图书: this.name = “Java多线程”
Note right of Thread01:Thread01开始sleep
Thread02->共同的图书: this.name = “JVM虚拟机”
Note left of Thread02:Thread02开始sleep
Thread02->共同的图书: this.code = “10002”(Thread02抢到了CPU,先给赋值)
Thread01->共同的图书: this.code = “10001”
Thread01->共同的图书: 打印bookName: JVM虚拟机, code: 10001
Thread02->共同的图书: 打印bookName: JVM虚拟机, code: 10001
$

2.有加synchronized的情况

$seq
Thread01->共同的图书: 获得线程锁
Thread01->共同的图书: this.name = “Java多线程”
Note right of Thread01:Thread01开始sleep
Thread01->共同的图书: this.code = “10001”
Thread01->共同的图书: 打印bookName: Java多线程, code: 10001
Thread01->共同的图书: 释放线程锁
Thread02->共同的图书: 获得线程锁
Thread02->共同的图书: this.name = “JVM虚拟机”
Note left of Thread02:Thread02开始sleep
Thread02->共同的图书: this.code = “10002”
Thread02->共同的图书: 打印bookName: JVM虚拟机, code: 10002
Thread02->共同的图书: 释放线程锁
$

总结

可见,在需要同步的方法上加上synchronized关键字,可以起到让jvm按照顺序来执行的效果。当有多个线程进行访问的时候,这时候只能“排队”一个线程一个线程来执行方法,从而让方法安全。

另外一个重点是,在示例中一直强调一个资源,因为只有多个线程对同一个资源进行操作的时候才会出现线程安全问题,当我们创建两个资源,有两个线程分别操作的时候,根本就不会出现线程安全问题,synchronized关键字也不会让方法同步执行,因为不同的对象,是不同的锁,这时候操作依然会是异步操作。

(二)脏读问题

如果set方法同步了,但是get方法没有进行同步,那么get方法是会异步请求的。

执行过程:

$seq
设值线程->共同的图书: 获得线程锁
设值线程->共同的图书: this.name = “Java多线程”
Note right of 设值线程: 设值线程开始sleep
共同的图书->取值线程: 打印bookName: Java多线程, code:null
Note left of 取值线程: 因为取值没有同步所以进来了
设值线程->共同的图书: this.code = “10001”
设值线程->共同的图书: 释放线程锁
$

初步解决方式:给取值方法也加上synchronized,但是会引起性能问题。

这里有两个知识点:
1. 当一个同步方法被调用的时候,其他线程必须等待这个线程释放锁的时候才可以执行该方法或者其他synchronized方法
2. 当一个同步方法被调用的时候,另外一个方法如果没有同步,依然可以被异步调用

(三)锁重入

意思是:如果一个方法调用另外一个同步方法的时候,会立即获得锁,并且等这个方法执行完成以后才释放。

示例

修改setBookNameAndCode让他调用另外一个同步方法:

/**
 * 图书类
 * @author liweidan
 */
public class Book {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public synchronized void setBookNameAndCode(String name, Integer code) {
        try {
            this.name = name;
            Thread.sleep(2000L);
            this.bookCode = code;
            anohtherMethod();
            System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void anohtherMethod() {
        System.out.println("============ anohtherMethod ===========");
    }
}

结果

============ anohtherMethod ===========
bookName: Java多线程, code: 10001
============ anohtherMethod ===========
bookName: JVM虚拟机, code: 10002

(四)锁无继承性

当父类一个同步方法被子类重写的时候,依然需要加上同步标记

(五)总结

  1. 当方法加上synchronized关键字的时候,线程需要排队执行这个方法
  2. 当A线程调用一个同步方法的时候,B线程可以调用其他的非同步的方法
  3. 如果有两个同步方法,A线程在执行第一个方法的时候,B线程需要等待A线程的锁才可以进行执行另外一个同步方法
  4. 锁重入,当一个同步方法执行另外一个同步方法的时候,该线程可以立即获得锁继续执行
  5. 无继承性:子类重写父类同步方法的时候,需要再加上synchronized关键字

三、synchronized修饰代码块

(一)锁是对象自己的时候

synchronized方法的弊端:锁住整个方法,当方法中有些操作不需要同步的时候,也一起同步了,造成了不必要的性能损失。

synchronized方法的锁对象也是本对象,所以当我们用代码块锁锁住this的时候,其实性能是基本没有差别的。

示例

简单小示例来做一下,还是上面的例子,不过通过模拟耗时的不需要同步的操作来模仿后续

/**
 * 使用对象锁锁代码块
 * @author liweidan
 * @date 2017.12.15 下午5:15
 * @email toweidan@126.com
 */
public class BookObjLock {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public void setBookNameAndCode(String name, Integer code) {
        int aNum = 0;
        for (int i = 0; i < 100000; i++) {
            // 其他事情
            aNum += i;
        }
        System.out.println(Thread.currentThread().getName() + ": " + aNum);
        synchronized (this) {
            try {
                this.name = name;
                Thread.sleep(2000L);
                this.bookCode = code;
                System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 图书业务类
 * @author liweidan
 * @date 2017.12.15 下午3:48
 * @email toweidan@126.com
 */
public class BookObjLockService implements Runnable {
    BookObjLock book;
    String name;
    Integer code;
    public BookObjLockService(BookObjLock book, String name, Integer code) {
        this.book = book;
        this.name = name;
        this.code = code;
    }
    @Override
    public void run() {
        book.setBookNameAndCode(name, code);
    }
}

public static void testObjLock() {
    /** 创建一个共享资源 */
    BookObjLock book = new BookObjLock();
    /** 创建两个线程来对这个共享资源进行修改 */
    Thread t1 = new Thread(new BookObjLockService(book, "Java多线程", 10001), "Thread01");
    Thread t2 = new Thread(new BookObjLockService(book, "JVM虚拟机", 10002), "Thread02");
    t1.start();
    t2.start();
}

结果

/** 这里是长操作,无同步 */
Thread01: 704982704
Thread02: 704982704
bookName: Java多线程, code: 10001
bookName: JVM虚拟机, code: 10002

可以看到,需要长时间的操作并不会同步执行。这样子就能大大提高项目的性能。

(二)锁是其他对象

代码块中,锁可以设置为其他对象,这样子就可以更进一步控制同步的时机。

示例

对设置namecode的方法加上不同的锁,以便在设置书名或者书编号的时候可以不被干扰。

public class BookObjLock {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public void setBookNameAndCode(String name, Integer code) {
        int aNum = 0;
        for (int i = 0; i < 100000; i++) {
            // 其他事情
            aNum += i;
        }
        System.out.println(Thread.currentThread().getName() + ": " + aNum);
        synchronized (this) {
            try {
                this.name = name;
                Thread.sleep(2000L);
                this.bookCode = code;
                System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public Integer getBookCode() {
        return bookCode;
    }
    public void setBookCode(Integer bookCode) {
        synchronized ("code") {
            this.bookCode = bookCode;
            System.out.println("setBookCode============= name: " + this.name + ", code: "+this.bookCode + ", current" + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName() + "setBookCode over");
        }
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        synchronized ("name") {
            this.name = name;
            System.out.println("setName============= name: " + this.name + ", code: "+this.bookCode + ", current" + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName() + "setName over");
        }
    }
    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", bookCode=" + bookCode +
                '}';
    }
}

/** 两个线程 */
/**
 * 
 * @author liweidan
 * @date 2017.12.15 下午5:45
 * @email toweidan@126.com
 */
public class BookNameThread implements Runnable {
    BookObjLock book;
    String name;
    public BookNameThread(BookObjLock book, String name) {
        this.book = book;
        this.name = name;
    }
    @Override
    public void run() {
        book.setName(name);
    }
}

/**
 * 
 * @author liweidan
 * @date 2017.12.15 下午5:45
 * @email toweidan@126.com
 */
public class BookCodeThread implements Runnable {
    BookObjLock book;
    Integer code;
    public BookCodeThread(BookObjLock book, Integer code) {
        this.book = book;
        this.code = code;
    }
    @Override
    public void run() {
        book.setBookCode(code);
    }
}

public static void testMultiLock() {
    /** 创建一个共享资源 */
    BookObjLock book = new BookObjLock();
    Thread t1 = new Thread(new BookCodeThread(book, 10001), "t1-setCode");
    Thread t2 = new Thread(new BookCodeThread(book, 10002), "t2-setCode");
    Thread t3 = new Thread(new BookNameThread(book, "JVM虚拟机"), "t3-setName");
    t1.start();
    t2.start();
    t3.start();
}

结果

setBookCode============= name: null, code: 10001, current1513331932626
setName============= name: JVM虚拟机, code: 10001, current1513331932626
t1-setCodesetBookCode over
t3-setNamesetName over
setBookCode============= name: JVM虚拟机, code: 10002, current1513331932626
t2-setCodesetBookCode over

可以看到,设置code方法和设置name方法是异步执行的,只有是当两个线程抢到同一个锁的时候,才会同步执行。但是如果锁不是同一个,那么会是异步运行。

换句话说,通过控制不同方法锁不同的对象,可以控制线程执行不同方法的同步性。

注意:由于String对象具有一旦创建就放入常量池的特性,所以在加锁的时候,应该避免使用String作为线程锁,取而代之的是使用new Object()作为线程锁,并且这个对象不放入缓存中(即为了避免是同一个对象而让我们误以为是不同对象造成业务出错)

(三)嵌套锁与死锁

嵌套锁:就是一个同步代码块中,又有另外一个代码块。多用于一个方法修改两个对象,需要锁住不同对象的时候,但是其实开发中比较少遇到这种情况。

死锁:嵌套锁情况下,两个锁出现了互相等待,造成程序卡主无法执行下去。

示例

直接使用死锁来看嵌套锁的例子。

/**
 * 演示死锁
 * @author liweidan
 * @date 2017.12.16 下午2:35
 * @email toweidan@126.com
 */
public class DeadLine implements Runnable {
    private String userName;
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    public void setUserName(String userName) {
        this.userName = userName;
    }
    @Override
    public void run() {
        if (userName.equals("a")) {
            synchronized (lock1) {
                doSomething();
                synchronized (lock2) {
                    System.out.println("Lock1 -> Lock2");
                }
            }
        } else if (userName.equals("b")) {
            synchronized (lock2) {
                doSomething();
                synchronized (lock1) {
                    System.out.println("Lock2 -> Lock1");
                }
            }
        }
    }
    private void doSomething() {
        try {
            System.out.println("username = " + userName);
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public static void testDeadlock() {
    try {
        DeadLine deadLine = new DeadLine();
        deadLine.setUserName("a");
        Thread t1 = new Thread(deadLine);
        t1.start();
        Thread.sleep(100);
        deadLine.setUserName("b");
        Thread t2 = new Thread(deadLine);
        t2.start();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

结果:

username = a
username = b

通过结果可以看到,两个线程卡住了,而程序并未运行结束,一直跑着。

执行时序图

$seq
线程t1->DeadLine: 创建了A的userName并且执行
线程t1->DeadLine: 获得Lock1
线程t1->DeadLine: 线程在run方法内睡眠3秒
线程2->DeadLine: 300秒被创建,B的userName并且执行
线程2->DeadLine: 获得Lock2
线程2->DeadLine: 线程在run方法内睡眠3秒
线程t1->DeadLine: 醒来,等待Lock2才能继续执行
线程2->DeadLine: 醒来,等待Lock1才能继续执行
Note right of DeadLine: 正是这一步导致死锁
$

四、volatile关键字

(一)概念

volatile修饰了变量后,当有多个线程拿到这个变量的时候,能够拿到最新的值。

jvm有主内存、线程栈的概念,当线程A改变了变量的值的时候,而线程B读取该值的时候,并不能够马上读到新的值,因为线程A改变的值并不会相应到线程B的线程栈中,要解决这个问题,加上volatile后,每次线程B取值的时候都会从公共堆中去取值。

接下来使用程序以及时序图来帮助理解

(二)具体示例

1. 未加上volatile

/**
 * @author liweidan
 * @date 2017.12.16 下午3:48
 * @email toweidan@126.com
 */
public class VolatileThread implements Runnable {
    private boolean isRunning = true;
    public void changeRunning() {
        isRunning = !isRunning;
    }
    public boolean isRunning() {
        return isRunning;
    }
    @Override
    public void run() {
        System.out.println("============== start ==============");
        while (isRunning) {
        }
        System.out.println("================= End =============");
    }
    public static void main(String[] args) {
        try {
            /** 创建一个资源 */
            VolatileThread thread = new VolatileThread();
            /** 启动一个线程执行run方法 */
            Thread t1 = new Thread(thread);
            t1.start();
            /** 修改变量的值 */
            Thread.sleep(3000L);
            thread.changeRunning();
            System.out.println(thread.isRunning);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果如下:

============== start ==============
false

2. 加上volatile

public class VolatileThread implements Runnable {
    private volatile boolean isRunning = true;
    .......
}

结果如下:

============== start ==============
false
================= End =============

可以看出来:
– 未加上volatile的时候,虽然主线程在3秒以后把isRunning进行了修改,但是线程停不下来。
– 加上volatile的时候,主线程在3秒以后把isRunning进行了修改,线程停下来了。

3. 分析结果差异

老方法,时序图分析过程。

对于未加上volatile的时候,程序是这么执行的:

$seq
线程t1->公共堆: 从主内存中读取isRunning变量
线程t1->线程栈: 写入isRunning=true
线程t1->线程栈: isRunning=true,写到线程栈,while循环
线程Main->公共堆: 等待3秒后,把isRunning改为false
线程Main->公共堆: 输出isRunning=false,结束
线程t1->线程栈: 读取线程栈isRunning=true,while一直循环
$

加上volatile的时候,程序是这么执行的:

$seq
线程t1->公共堆: 从主内存中读取isRunning变量
线程t1->线程栈: 读取isRunning=true,while开始循环
线程Main->公共堆: 等待3秒后,把isRunning改为false
线程Main->公共堆: 输出isRunning=false,结束
线程t1->公共堆: 读取线程栈isRunning= false,while停止
$

可以看出,当变量加上volatile的时候,那么强制线程栈每次读取变量的时候都从公共堆里面去读,所以能够读到最新的值。

(三)非原子性

1. 原子性问题

/**
 * 测试非原子性
 * @author liweidan
 * @date 2017.12.16 下午4:24
 * @email toweidan@126.com
 */
public class VolatileThread02 implements Runnable {
    private static int count;
    private static void addCount() {
        for (int i = 0; i < 100; i++) {
            count++;
        }
        System.out.println(count);
    }
    @Override
    public void run() {
        addCount();
    }
    public static void main(String[] args) {
        VolatileThread02[] ts = new VolatileThread02[100];
        Thread[] threads = new Thread[100];
        for (int i = 0; i < 100; i++) {
            ts[i] = new VolatileThread02();
            threads[i] = new Thread(ts[i]);
        }
        for (int i = 0; i < 100; i++) {
            threads[i].start();
        }
    }
}

/**  结果: */
....
9493
9593
9693
9793
9893
9993

结果并没有如我们所愿,一百一百的增加。所以我们需要给addCount()方法加上synchronized关键字才能让变量变成原子性的增加。

所以关键字volatile并不是说可以保证变量的安全,如需保证安全性依然需要使用synchronized关键字,关键字volatile只是为了保证变量在被取值的时候,都是最新的状态。

2. i++问题

上面讲到i++需要三个步骤:
a. 读取i的值
b. 新增
c. 写入主存

但是如果加上volatile关键字,是可以保证第a步,但是如果此时又被其他线程读取执行了第a步,那么就会出现脏数据

原子类AtomicXXX

AtomicXXX是类似于i++操作的原子类,后面的XXX可以替换成Java包装类的名字。

/**
 * 原子类Demo
 * @author liweidan
 * @date 2017.12.16 下午4:38
 * @email toweidan@126.com
 */
public class AtomicDemo extends Thread {
    private AtomicInteger count = new AtomicInteger(0);
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            /** 新增并且获取 */
            System.out.println(count.incrementAndGet());
        }
    }
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo();
        Thread t1 = new Thread(atomicDemo);
        Thread t2 = new Thread(atomicDemo);
        Thread t3 = new Thread(atomicDemo);
        Thread t4 = new Thread(atomicDemo);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

...
39994
39995
39996
39997
39998
39999
40000

结果可以安全的新增到40000

注意:虽然原子类是安全的,但是需要方法调用之间的安全性。

3. synchronized可以具备可见性

synchronized关键字可以是多线程访问同一个变量具有同步性,而且还具备将工作内存中的变量与公共内存中的变量同步的功能。

五、总结

(一)synchronized:

  1. 修饰方法可以让方法执行具有同步性,多个线程时需要依次逐个进入执行
  2. 修饰代码块,指定对象锁,当线程获取进入代码块的时候,其他线程需要等待进入
  3. 修饰代码块,指定多个对象锁,当线程获取进入代码块的时候,其他线程如需要进入同个代码块里,需要等待,但是如果进入的是另外一个锁的代码块,则可以和其他线程异步进行
  4. 脏读:当设值的时候进行了同步,但是可能设值和读取值的线程异步进行导致获取值是脏的数据。解决:给取值方法加上同步
  5. i++的原子性:读取、操作、赋值
  6. 锁重入:当一个加锁的方法或者代码块调用另外一个加锁的方法,可以立即获取锁
  7. 无继承性:重写父类同步方法需要重新声明

(二)volatile:

  1. 内存间的可见性
  2. 非原子性

[多线程]Java多线程02_线程操作

1. 线程常用方法

1)currentThread()

获取当前线程的对象信息,获取该对象就可以获取该线程的信息。常用方法是getName(),可以获取当前线程的名字。

  • 编写线程类
public class CurrentDemo implements Runnable {

    public CurrentDemo() {
        System.out.println("构造方法被调用,当前线程:" + Thread.currentThread().getName());
    }


    @Override

    public void run() {
        System.out.println("Run方法被调用,当前线程:" + Thread.currentThread().getName());
    }
}
  • 启动
public static void startVurrentDemo() {
    /** 由main调用构造方法 */
    CurrentDemo demo = new CurrentDemo();

    /** 由线程1调用run方法 */
    Thread t1 = new Thread(demo, "线程1");
    t1.start();
}
  • 结果
构造方法被调用,当前线程:main
Run方法被调用,当前线程:线程1

由结果可知,调用构造方法的也是一个线程,而且是main线程,而调用run方法的就是自己创建出来的一个线程了。

2)isAlive()

方法作用:用于测试线程是否处于活动状态。
活动状态:线程已经启动(调用start()方法),尚未结束。线程处于正在运行或者准备开始运行状态,则认为线程是“存活”的。

  • 线程类:
public class AliveDemo implements Runnable {

    public AliveDemo() {
        System.out.println("构造方法被调用");
        System.out.println("当前线程是:" + Thread.currentThread().getName());

        /** 注意调用当前的线程是Main,所以调用isAlive的时候返回的是true */
        System.out.println("isAlive: " + Thread.currentThread().isAlive());
        System.out.println("构造方法END");
    }

    @Override
    public void run() {
        System.out.println("RUN方法被调用");
        System.out.println("当前线程是:" + Thread.currentThread().getName());
        System.out.println("isAlive: " + Thread.currentThread().isAlive());
        System.out.println("RUN方法END");
    }
}
  • 启动类
public static void startIsAlive() {
    Thread t1 = new Thread(new AliveDemo(), "线程1");
    t1.start();
    System.out.println("Main: isAlive: " + t1.isAlive());
}
  • 结果
构造方法被调用
当前线程是:main
isAlive: true
构造方法END
RUN方法被调用
当前线程是:线程1
isAlive: true
RUN方法END
Main: isAlive: true

注意:在调用start的后面加上当前线程睡眠1秒钟,那t1输出的结果就不同了(变为false),因为在这个例子中,调用isAlive()的时候刚好线程还没运行结束

3)sleep()

方法作用:让当前线程沉睡指定的毫秒数。

  • 线程类:
public class SleepDemo implements Runnable {


    @Override
    public void run() {
        try {
            System.out.println("RUN开始,当前时间:" + System.currentTimeMillis());
            Thread.sleep(1000L);
            System.out.println("RUN结束,当前时间:" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 启动类
public static void startSleep() {
    Thread t1 = new Thread(new SleepDemo(), "睡眠线程");
    t1.start();
}
  • 结果
RUN开始,当前时间:1512970671432
RUN结束,当前时间:1512970672436

4)getId()

方法作用:获取线程唯一id值

2. 操作线程方法

1)停止线程

interrupt(): 给线程打上中断标记
interrupted(): 测试当前线程是否已经中断,执行后将状态标志设置为false(调用该方法后再调用第二次会返回false)
isInrrupted(): 测试线程Thread对象是否已经是中断状态,但不清除标志。

① interrupt()示例

  • 线程类
public class Stop01Demo implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 500000; i++) {
            System.out.println(i);
        }
    }
}
  • 启动类
public static void startStop01() {
    Thread t1 = new Thread(new Stop01Demo(), "测试停止标记");
    t1.start();
    try {
        Thread.sleep(200L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t1.interrupt();
}
  • 结果
...
499994
499995
499996
499997
499998
499999

根据结果来看,调用interrupt()并没有停止线程,而是为线程做了标记,配合interrupted()(1. 测试当前线程; 2. 有清除功能)和isInrrupted()(无清除功能)在run方法内部来做线程的退出。

② interrupted()示例

  • 线程类和启动类合一
public static void startInterrupted() {
    Thread.currentThread().interrupt();
    System.out.println("是否停止1:" + Thread.interrupted());
    System.out.println("是否停止2:" + Thread.interrupted());// 已经清除标记,所以返回 false

    /** 重新打上停止的标记 */
    Thread.currentThread().interrupt();
    System.out.println("是否停止3:" + Thread.interrupted());
}
  • 结果
是否停止1:true
是否停止2:false
是否停止3:true

③ isInterrupted()示例

  • 线程类
public class Stop02Demo implements Runnable {
    @Override
    public void run() {
        while (true) {
        }
    }
}
  • 启动类
public static void startStop02() {
    Thread t1 = new Thread(new Stop02Demo());
    t1.start();
    t1.interrupt();
    System.out.println("是否停止:" + t1.isInterrupted());
    System.out.println("是否停止:" + t1.isInterrupted());
}
  • 结果
是否停止:true
是否停止:true

那么结合isInterrupted()这个方法,我们可以在我们的逻辑里面做相关的退出操作:

  • 线程类:
public class Stop03Demo implements Runnable {
    @Override
    public void run() {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("END!");
                return;
            }
        }
    }
}

④ 异常退出法

我们知道当线程发生异常的时候,如果没有使用try-catch进行捕获处理的话,那么线程是会退出运行的。那么只需要把上面的return改成抛出一个运行时异常即可,但是不知道为啥,我始终认为这个方法不够优雅。

⑤ sleep()和interrupt()

  • sleep()interrupt()
  • interrupt()sleep()
1. 先sleep()interrupt()
  • 线程类
public class SleepAndInterruptDemo implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            System.out.println("InterruptedException!");
            e.printStackTrace();
        }
    }
}
  • 启动类
public static void startSleepAndInterrupt() {
    Thread t1 = new Thread(new SleepAndInterruptDemo());
    t1.start();
    t1.interrupt();
}
  • 结果
InterruptedException!
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at p02.threadClz.SleepAndInterruptDemo.run(SleepAndInterruptDemo.java:7)
    at java.lang.Thread.run(Thread.java:748)

结果刚好就是sleep方法需要捕获的异常

2. 先interrupt()sleep()
  • 线程类
public class InterruptAndSleepDemo implements Runnable {
    @Override
    public void run() {
        try {
            for (int i = 0; i < 100000; i++) {
                /** 拖延时间 */
                System.out.println(i);
            }
            System.out.println("Sleep");
            Thread.sleep(200000L);
            System.out.println("Sleep End");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 启动类
public static void startInterruptAndSleep() {
    Thread t1 = new Thread(new InterruptAndSleepDemo());
    t1.start();
    t1.interrupt();
}
  • 结果
...
99998
99999
Sleep
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at p02.threadClz.InterruptAndSleepDemo.run(InterruptAndSleepDemo.java:11)
    at java.lang.Thread.run(Thread.java:748)

就是睡不了

⑥ stop()示例

stop方法是一个已经被官方定义为过时的方法,因为这个方法是有缺陷的,它导致线程的后需清理工作做的不到位。所以在生产环境中禁止去使用stop方法进行线程的停止。

2)暂停线程

① 对应的方法有suspend()resume()

  • 线程类
public class SuspendDemo implements Runnable {
    public int i = 0;
    @Override
    public void run() {
        while (true) {
            i++;
        }
    }
}
  • 启动类
public static void startSuspendDemo() throws InterruptedException {
    SuspendDemo demo = new SuspendDemo();
    Thread t1 = new Thread(demo, "Suspend Thread");
    t1.start();

    /** 暂停运行 */
    t1.suspend();
    System.out.println("当前时间:" + System.currentTimeMillis() + ", i = " + demo.i);
    Thread.sleep(5000);
    System.out.println("当前时间:" + System.currentTimeMillis() + ", i = " + demo.i);

    /** 恢复运行 */
    t1.resume();
    Thread.sleep(5000);

    /** 再次暂停 */
    t1.suspend();
    System.out.println("当前时间:" + System.currentTimeMillis() + ", i = " + demo.i);
    Thread.sleep(5000);
    System.out.println("当前时间:" + System.currentTimeMillis() + ", i = " + demo.i);
}
  • 结果
当前时间:1512978809877, i = 205824
当前时间:1512978814881, i = 205824
当前时间:1512978819884, i = -1595043396
当前时间:1512978824887, i = -1595043396

② 占用同步资源

  • 资源类
public class SynchronizedObject {

    public synchronized void doSomething() {
        System.out.println("begin doSomething");
        if (Thread.currentThread().getName().equals("Thread01")) {
            System.out.println("暂停当前线程");
            Thread.currentThread().suspend();
        }
        System.out.println("END");
    }

}
  • 线程类
public class SuspendDemo2 implements Runnable {

    private SynchronizedObject synchronizedObject;

    public SuspendDemo2(SynchronizedObject synchronizedObject) {
        this.synchronizedObject = synchronizedObject;
    }

    @Override
    public void run() {
        synchronizedObject.doSomething();
    }
}
  • 运行类
public static void startSuspendDemo2() {
    SynchronizedObject synchronizedObject = new SynchronizedObject();
    Thread t1 = new Thread(new SuspendDemo2(synchronizedObject), "Thread01");
    Thread t2 = new Thread(new SuspendDemo2(synchronizedObject), "Thread02");

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

}
  • 结果
begin doSomething
暂停当前线程

可以看到t1线程在执行资源占用锁的时候,停止了,但是并没有把锁释放出来让其他线程用。

③ 另外,suspendresume会导致数据不一致

这个问题,跟资源有没有加锁是有关系的,但是加锁了会出现上面第②独占锁的情况,不加的话,资源不安全也是正常的,所以,不使用suspendresume才是正确的选择,后期会说到synchronize锁的问题,可以解决暂停以及重新恢复的问题。

3)让出资源

yield(): 让出当前CPU资源,但是可能刚刚让出资源,就又被该线程抢到了。

  • 线程类
public class YieldDemo implements Runnable {

    @Override
    public void run() {
        long start = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; i < 50000000; i++) {
            /** 这里指定一个线程名字用来让出CPU */
            if (Thread.currentThread().getName().equals("YieldThread")) {
                Thread.yield();
            }
            count += i;
        }
        System.out.println(Thread.currentThread().getName() + "耗时:" + (System.currentTimeMillis() - start));
    }

}
  • 启动类
public static void startYield() {
    YieldDemo demo = new YieldDemo();
    Thread t1 = new Thread(demo, "YieldThread");
    Thread t2 = new Thread(demo, "YieldThread2");
    t1.start();
    t2.start();
}
  • 结果
YieldThread2耗时:120
YieldThread耗时:11132

可以看出来,让出CPU的线程执行的非常慢。

4)优先级

通过线程对象调用setPriority(int newPriority)来设置线程的优先级。优先级的范围是1-10,如果不是就会抛出异常。

需要注意:并不是优先级高的任务会先执行完,而是说优先级高的线程会更有机会获得CPU时间片,从而更快的执行相对应的任务。

特性:
– 继承
– 规则性
– 随机性

通过优先级来模拟龟兔赛跑

  • 动物类
public class Animal implements Runnable {
    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            System.out.println(Thread.currentThread().getName());
        }
        System.out.println(Thread.currentThread().getName() + "跑完了100米");
    }
}
  • 线程
public static void rabbitAndTurtle() {
    Thread t1 = new Thread(new Animal(), "兔子");
    Thread t2 = new Thread(new Animal(), "乌龟");

    t1.setPriority(Thread.MAX_PRIORITY);// 兔子比较快
    t2.setPriority(Thread.MIN_PRIORITY);// 乌龟比较慢

    t2.start();
    t1.start();
}
  • 结果

结果太长了,但是每次运行“均”都是龟比较慢跑完

3. 守护线程

守护线程:当主线程退出运行的时候,启动的线程也就退出运行。如:GC(垃圾回收器),当Java线程退出的时候,它的存在也就失去了意义,所以他也退出了运行。默认是关闭的。

  • 线程类
public class DaemonDemo implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("当前时间是: " + LocalDate.now());
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 启动类
public static void startDaemon() {
    Thread t1 = new Thread(new DaemonDemo(), "守护线程");
    t1.setDaemon(true);
    t1.start();
    try {
        Thread.sleep(5000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
  • 结果
当前时间是: 2017-12-11
当前时间是: 2017-12-11
当前时间是: 2017-12-11
当前时间是: 2017-12-11
当前时间是: 2017-12-11

当设置了t1.setDaemon(true);的时候,主线程退出运行,则其启动的线程也退出了运行。

[多线程]Java多线程01_初入门

1. 线程与进程的区别

进程 > 线程

进程:相当于启动一个软件,就启动了一个进程。当启动了QQ以及微信,那么这两个不同软件运行的独立任务就是两个不同的进程

线程:一个软件了里面,为了进行多个任务,就需要多个线程来协同工作,以可以尽可能多的使用计算机的资源。

注:由于CPU在线程、进程之间切换非常快,所以给人一种这些进程、线程都是在一起工作的错觉。

2. Java中的线程使用

Java是一门多线程的语言,而且,不论在安卓开发也好、服务器开发也好、桌面开发也好,都很多的使用了线程来完成多个任务。

安卓开发:使用多个线程,来执行多个任务,比如页面的同时渲染,在发送数据的时候确保Socket连接等等

服务器开发:最常见的SpringMVC开发,其实整个服务器只实例化一个DispatcherServlet,而能够支持多个用户同时在线操作的原因就是因为,SpringMVC使用了线程来对数据进行很好的隔离,从而使数据都是安全的。

桌面开发:跟安卓差不多。

需要说明的是,我们知道,Java程序的入口是main方法,而main方法本身就是一个线程,具体可以通过在main方法中调用currentThread()获取相对应的线程然后再通过getName()方法获取对应的名字,可以在控制台打印出来得到main的字符串

Java中使用多线程也是非常方便的,有两种情况,一种是继承Thread类,另外一种就是实现Runnable接口

1)继承Thread类

  • 编写线程类
package p01.threadClass;

/**
 * 通过继承Thread方式开发线程
 */
public class ExtendThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

  • 启动
public static void startExtThread() {
    ExtendThread extendThread1 = new ExtendThread();
    ExtendThread extendThread2 = new ExtendThread();
    ExtendThread extendThread3 = new ExtendThread();

    extendThread1.setName("Thread-01");
    extendThread2.setName("Thread-02");
    extendThread3.setName("Thread-03");

    extendThread1.start();
    extendThread2.start();
    extendThread3.start();
}
  • 打印:
Thread-01
Thread-03
Thread-02

这样就可以编写自己的线程任务了。

需要注意的是:
1. 虽然我们在main方法中按顺序调用三个线程,但并不意味着这三个线程会按照顺序运行,而是通过抢夺资源,哪个线程抢到了,就先运行,所以控制台打印的顺序也没有按照启动的顺序来;
2. 因为Java是单继承的关系,所以,并不推荐使用继承的方式来开发,而是通过下面实现接口的方式来做。

2)实现Runnable接口(推荐使用)

  • 编写线程类
package p01.threadClass;

/**
 * 实现Runable
 */
public class ImplementThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

  • 启动
public static void startImplThread() {
    /** 需要通过传入我们的实现的线程的对象来创建线程 */
    Thread thread1 = new Thread(new ImplementThread());
    Thread thread2 = new Thread(new ImplementThread());
    Thread thread3 = new Thread(new ImplementThread());

    thread1.setName("Thread-01");
    thread2.setName("Thread-02");
    thread3.setName("Thread-03");

    thread1.start();
    thread2.start();
    thread3.start();
}
  • 控制台
Thread-01
Thread-02
Thread-03

效果和继承是一样的,只不过通过实现,在Java开发思想中更加符合,也能规避单继承的缺陷。

3. 初识线程安全

出现线程安全问题,我们需要让多个线程共享同一个变量,当多个线程读取变量的时候,如果没有对该变量做任何限制,则会出现线程安全的问题。

  • 线程类
public class ThreadSafeDemo extends Thread {

    private int n = 5;

    @Override
    public void run() {
        n--;
        System.out.println("由" + this.currentThread().getName() + "执行,count = " + n);
    }   
}
  • 启动类
public static void startThreadSafeDemo() {
    ThreadSafeDemo demo = new ThreadSafeDemo();
    Thread th1 = new Thread(demo, "A");
    Thread th2 = new Thread(demo, "B");
    Thread th3 = new Thread(demo, "C");
    Thread th4 = new Thread(demo, "D");
    Thread th5 = new Thread(demo, "E");

    th1.start();
    th2.start();
    th3.start();
    th4.start();
    th5.start();
}
  • 结果
由B执行,count = 2
由D执行,count = 1
由A执行,count = 2
由C执行,count = 2
由E执行,count = 0

结果得到的是一个全部乱套了的数据.

原因:i--不是一个原始性操作,在jvm内部,i--分为三步运行:

1. 取得原有i的值
2. 计算i-1
3. 对i进行赋值

那么在这个过程中,可能线程A在执行第一步和第二步的时候,CPU资源就被线程B抢走了,导致A对i进行了减一操作,但是还没把值赋回去,而线程B获取的值是还没减一操作的值,所以这个过程导致了线程安全的问题。

1)初步解决方法

run方法加上synchronized关键字,可以解决,但是速度较慢,后续会慢慢根据这个思路进行优化。

  • 线程类代码片段
@Override
public synchronized void run() {
    n--;
    System.out.println("由" + this.currentThread().getName() + "执行,count = " + n);
}
  • 结果
由A执行,count = 4
由B执行,count = 3
由C执行,count = 2
由D执行,count = 1
由E执行,count = 0

原因:加上synchronized关键字,可以保证一个线程在该方法内部进行计算的时候,不能被其他线程抢走资源,也就是说,当线程A在执行i--上面那三个操作的时候,只能等到线程A执行完成,才能被其他线程获取。

4. 总结

到这里就已经把Java开发线程基本步骤给学完了,接下来就是要管理线程的生命周期。例如启动、暂停、停止等等,还有一系列的线程安全问题。

打算开始写数据结构相关的东西

其实我作为一个开发工程师但是本身并不是计算机专业出身的,这让我在应聘的时候有点折扣,好在现在还有自学考试这个东西,所以我报名了自考。

学数据结构这个东西,也不是自考趋势我去看,因为在很多网站,很多高手看来,像计算机专业的一些课程比如操作系统、数据结构等等,都是比较重要的,所以带着这个来做。

数据结构在我看来,就是在平时编码过程中可以找到解决问题的最优方式(可能不是最优,但是比较好的),所以要说不重要那是不可能的,就像高中的时候做数学题,找到最优方式,可以省点笔墨,维护起来也比较舒服。

所以带着以前老师常说的4W1H来学这个东西。也是我最近想起来的一个东西,在写一个知识点的时候先记录这几个规则,会让知识点显得更清晰。

使用自考带的教科书籍,难受,看都没看懂,主要是语法格式也是让我难受的一部分,所以直接使用外面推荐的书籍《大话数据结构》,这本书写的很好,通俗易懂,感觉也是老师在上课的时候的讲话实录,有一种自己在上课的感觉。所以写数据结构的时候我就使用这本书来看,配图也是截图出来的。

[jdk8] jdk8 多线程处理的使用

github地址:https://github.com/WeidanLi/Java-jdk8-future

一、Future接口

Future是jdk5的时候被引入的,目的是为了把耗时的操作解放出来,可以同时使用多核的优势进行并行处理。比如,我有一个页面,需要从多方获取数据比如说从Twitter和Facebook获取数据,然后一起渲染的页面上。这时候如果等待Twitter的数据获取完在获取FB的数据,就显得比较慢了,这时候可以通过Future来让这两个任务并行处理。

阅读更多

[jdk8]jdk8并行处理数据

一、简述

JDK8为了让处理大数据量集合更快速,使用了并行的形式来进行处理。在上面的例子中我们也看到了,如果我们需要一个并行流的话,只要对一个集合打开parallelStream即可。在JDK7以前,想要对一个集合进行并行处理似乎是一件困难的事情。所以这一篇文章我们可以看看JDK8是怎么实现并行处理的。

阅读更多

[jdk8]jdk8——流的收集器

之前写了JDK8集合流的入门以及筛选,也就是集合流使用的打开和中间操作。这次带来的是不同的收集数据的方式。

本节代码GitHub地址:https://github.com/WeidanLi/Java-jdk8-collect

一、准备:

还是老规矩,使用菜单进行示例。(代码的话建议拷贝这部分)

二、收集器简介

收集器即收集东西的容器,它用于使用集合流的时候的终端操作,即我们在日常的业务逻辑中把流进行过滤也好,进行筛选也好,然后我们总该要有一个容器可以存放这些过滤后的元素。这时候的收集器就派上用场了。如代码所示一个最简单的收集器的使用实例(当然我感觉平时应该没人这么无聊)

阅读更多

[jdk8]jdk8——流的增强、流的中间操作以及终端操作

示例代码:https://github.com/WeidanLi/Javajdk8stream.git

零、准备

这一个部分,我们准备了菜单。包含菜单名字、是否是素食、卡路里数量以及菜单类型四个属性,使用了Lombok自动加入GETTER&SETTER这些元素。

package cn.liweidan.pojo;

import lombok.*;

/**
 * <p>Desciption:</p>
 * CreateTime : 2017/6/6 下午2:51
 * Author : Weidan
 * Version : V1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public class Dish {

    /** 名字 */
    private String name;
    /** 是否素食 */
    private boolean vegetarain;
    /** 卡路里 */
    private int colories;
    /** 类型 */
    private Type type;

    public enum Type {MEAT, FISH, OTHER};

}

并且准备了一些菜单。

package cn.liweidan.utils;

import cn.liweidan.pojo.Dish;

import java.util.Arrays;
import java.util.List;

/**
 * <p>Desciption:</p>
 * CreateTime : 2017/6/6 下午2:55
 * Author : Weidan
 * Version : V1.0
 */
public class DishUtils {

    public static List<Dish> getDishes(){
        return Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT),
                new Dish("beef", false, 700, Dish.Type.MEAT),
                new Dish("chicken", false, 400, Dish.Type.MEAT),
                new Dish("french fries", true, 530, Dish.Type.OTHER),
                new Dish("rice", true, 350, Dish.Type.OTHER),
                new Dish("season fruit", true, 120, Dish.Type.OTHER),
                new Dish("pizza", true, 550, Dish.Type.OTHER),
                new Dish("prawns", false, 300, Dish.Type.FISH),
                new Dish("salmon", false, 450, Dish.Type.FISH));
    }

}

一、集合的处理

在jdk7以及以前,如果我们需要从一个集合中取出一个符合我们所需要条件的变量的时候,就需要去遍历这个元素中的每一个元素,然后使用if语句进行判断,如果符合我们所要的条件,就把这个元素放入另外一个集合中去。

阅读更多

[Gecco]Java爬虫框架Gecco入门教程

一、介绍

萌生出来的写爬虫的心里,是有一次我在写Springjpa的小Demo的时候,苦于没有数据,想了想博客这种数据类型能够符合我的需求,所以想要通过爬虫把我博客里面的数据爬出来(其实可以通过数据库,但是自我感觉不优雅)。
找了找以前自己笔记中比较出名的Java爬虫框架,排在第一位的就是这个框架了Gecco,据我以前了解到这是一款只要定义好vo类配以jq选择器风格的注解就可以拿到我们自己想要的逻辑,一时兴起拿起键盘就开始看了。

官方介绍: Gecco是一款用java语言开发的轻量化的易用的网络爬虫。Gecco整合了jsoup、httpclient、fastjson、spring、htmlunit、redission等优秀框架,让您只需要配置一些jquery风格的选择器就能很快的写出一个爬虫。Gecco框架有优秀的可扩展性,框架基于开闭原则进行设计,对修改关闭、对扩展开放。

阅读更多