基于多线程解析
多线程原理
创建线程一:
- package myThread;
- /**
- * 利用继承中的特点
- * 将线程名称传递 进行设置
- */
- // 继承 Thread 类时, 由于 Thread 类实现了 Runable 接口,
- // 并且已经重写了 run 方法.
- public class MyThread1 extends Thread{
- public MyThread1(String name){
- super(name);
- }
- // 重写 run 方法, 直接定义线程要执行的代码
- @Override
- public void run(){
- for (int i = 0; i <10; i++) {
- //getName()方法 来自父类
- System.out.println(getName()+i);
- }
- }
- }
- package myThread;
- public class Test1 {
- public static void main(String [] args){
- System.out.println("这里是 main 线程");
- MyThread1 myThread1=new MyThread1("风车车");
- // 开启风车车线程
- myThread1.start();
- for (int i = 0; i < 10; i++) {
- System.out.println("打印家老练"+i);
- /* 从输出结果可以看出虽然先调度的是
- * 这里是 main 线程
- * 打印家老练 0
- * 打印家老练 1
- * 打印家老练 2
- * 打印家老练 3
- * 打印家老练 4
- * 打印家老练 5
- * 打印家老练 6
- * 打印家老练 7
- * 打印家老练 8
- * 打印家老练 9
- * 风车车 0
- * 风车车 1
- * 风车车 2
- * 风车车 3
- * 风车车 4
- * 风车车 5
- * 风车车 6
- * 风车车 7
- * 风车车 8
- * 风车车 9
- */
- }
- }
- }
程序启动运行 main 时候, java 虚拟机启动一个进程, 主线程 main 在 main()调用时候被创建.
随着调用 myThread1 的对象的 start 方法, 另外一个新的线程也启动了, 这样整个应用就在多线程下运行.
运行过程内存的存储解析
多线程执行时, 在栈内存中, 其实每一个执行线程都有一片自己所属的栈内存空间. 进行方法的压栈和弹栈.
图片. PNG
当执行线程的任务结束了, 线程自动在栈内存中释放了, 当所有的执行线程都结束了, 那么进程就结束了.
Thread 类
构造方法
public Thread() : 分配一个新的线程对象.
public Thread(String name) : 分配一个指定名字的新的线程对象.
public Thread(Runnable target) : 分配一个带有指定目标新的线程对象.
public Thread(Runnable target,String name) : 分配一个带有指定目标新的线程对象并指定名字.
常用方法
public String getName() : 获取当前线程名称.
public void start() : 导致此线程开始执行; Java 虚拟机调用此线程的 run 方法.
public void run() : 此线程要执行的任务在此处定义代码.
public static void sleep(long millis) : 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行).
public static Thread currentThread() : 返回对当前正在执行的线程对象的引用.
- package myThread;
- public class Test2 {
- public static void main(String[] args) {
- MyRunnable myRunnable = new MyRunnable();
- // myRunnable 对象 = 线程任务对象
- Thread thread1 = new Thread(myRunnable);// 默认名字线程
- Thread thread2 = new Thread(myRunnable,"线程 2");
- Thread thread3 = new Thread(myRunnable,"线程 3");
- thread1.start();
- thread2.start();
- thread3.start();
- for (int i = 0; i < 10; i++) {
- System.out.println("主线程"+i);
- }
- }
- }
- package myThread;
- public class MyRunnable implements Runnable{
- @Override
- public void run() {
- for(int i=0;i<10;i++){
- System.out.println(Thread.currentThread().getName()+","+i);
- }
- }
- }
通过实现 Runnable 接口, 使得该类有了多线程类的特征. run()方法是多线程程序的一个执行目标. 所有的多线程 代码都在 run 方法里面. Thread 类实际上也是实现了 Runnable 接口的类. 在启动的多线程的时候, 需要先通过 Thread 类的构造方法 Thread(Runnable target) 构造出对象, 然后调用 Thread 对象的 start()方法来运行多线程代码.
实际上所有的多线程代码都是通过运行 Thread 的 start()方法来运行的. 因此, 不管是继承 Thread 类还是实现 Runnable 接口来实现多线程, 最终还是通过 Thread 的对象的 API 来控制线程的, 熟悉 Thread 类的 API 是进行多线程 编程的基础.
Thread 和 Runnable 的区别
如果一个类继承 Thread, 则不适合资源共享. 但是如果实现了 Runable 接口的话, 则很容易的实现资源共享.
总结:
实现 Runnable 接口比继承 Thread 类所具有的优势:
适合多个相同的程序代码的线程去共享同一个资源.
可以避免 java 中的单继承的局限性.
增加程序的健壮性, 实现解耦操作, 代码可以被多个线程共享, 代码和线程独立.
线程池只能放入实现 Runable 或 Callable 类线程, 不能直接放入继承 Thread 的类.
补充:
在 java 中, 每次程序运行至少启动 2 个线程. 一个是 main 线程, 一个是垃圾收集线程.
因为每当使用 java 命令执行一个类的时候, 实际上都会启动一个 JVM, 每一个 JVM 其实在就是在操作系统中启动了一个进程.
匿名内部类来创建线程
作用: 方便的实现每个线程执行不同的线程任务操作.
方法: 使用匿名内部类的方式实现 Runnable 接口, 重新 Runnable 接口中的 run()方法.
- package myThread;
- public class NoNameInnerDemo1 {
- public static void main(String [] args){
- Runnable r = new Runnable(){
- public void run() {
- for (int i = 0; i < 10; i++) {
- System.out.println("张杰:" + i);
- }
- }};
- new Thread(r).start();
- for (int i = 0; i < 10; i++) {
- System.out.println("林志颖"+i);
- }
- }
- }
将 Runnable 也匿名之后的用法
- package myThread;
- import jdk.nashorn.API.tree.NewTree;
- public class NoNameInnerDemo {
- public static void main(String [] args){
- new Thread(new Runnable() {
- @Override
- public void run() {
- for(int i=0;i<10;i++){
- System.out.println("开启的线程一"+i);
- }
- }
- }).start();
- for (int i = 0; i < 10; i++) {
- System.out.println("主线程"+i);
- }
- }
- }
线程安全
我们来模拟电影院的售票窗口, 实现多个窗口同时卖 "战狼 2" 这场电影票(多个窗口一起卖这 100 张票.
需要窗口, 采用线程对象来模拟; 需要票, Runnable 接口子类来模拟.
- package sell_tickets;
- /**
- * @author lx
- * @date 2019/1/22 - 15:01
- * 需求: 电影院卖 2017 年 9 月 12 日晚 20 点 08 分的战狼二电影, 共 100 张, 分三个窗口购买
- * 分析: 1, 总数 100 张票(用 20 张模拟)
- * 2, 三个窗口卖票 相当于三个线程同时卖票
- * 3, 三个窗口卖 100 张票需要资源共享 要实现 Runnable 接口
- *
- * 结果:
- * 直接使用 Runnalbe 发现线程里面重复买出去了第 20 张票
- * 出现了安全问题, 这样是不对的,
- *
- * 分析问题的原因:
- * 线程 1 并不知道线程 3 准备把 20 这张座的牌卖出去,
- * 于是两个线程都卖出去了, 出现了线程的并行问题
- * 我们想到解决问题的方法:
- * 线程调度:
- * 1. 分时调度 控制执行的时间, 一个线程执行完 另一个再执行
- * 2. 抢占式调度 这里先不考虑 线程优先级问题
- */
- public class SellTickets {
- public static void main(String[] args) {
- // 创建线程对象
- Tickets tickets=new Tickets();
- // 创建三个线程
- Thread thread1 = new Thread(tickets,"窗口一");
- Thread thread2 = new Thread(tickets,"窗口二");
- Thread thread3 = new Thread(tickets,"窗口三");
- thread1.start();
- thread2.start();
- thread3.start();
- }
- }
- public class Tickets implements Runnable{
- int ticket=20;
- @Override
- public void run() {
- // 判断如果票是否已经卖完
- if(ticket>0){
- try {// 休眠 100 秒, 并解决异常
- Thread.sleep(100);
- while(ticket>0){
- System.out.println(Thread.currentThread().getName()+"已经售出了"+(ticket--)+"张");
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 输出结果:
- // 窗口二已经售出了 18 张, 窗口二已经售出了 17 张, 窗口二已经售出了 16 张,
- // 窗口二已经售出了 15 张, 窗口二已经售出了 14 张, 窗口二已经售出了 13 张,
- // 窗口二已经售出了 12 张, 窗口二已经售出了 11 张, 窗口二已经售出了 10 张,
- // 窗口二已经售出了 9 张, 窗口二已经售出了 8 张, 窗口二已经售出了 7 张,
- // 窗口二已经售出了 6 张, 窗口二已经售出了 5 张, 窗口二已经售出了 4 张,
- // 窗口二已经售出了 3 张, 窗口二已经售出了 2 张, 窗口二已经售出了 1 张,
- // 窗口三已经售出了 19 张, 窗口一已经售出了 20 张,
- // 可以看出出现了最后一张票并不是最后售出的, 出现了问题
- }else {
- System.out.println("票已经卖完了");
- }
- }
- }
线程同步
当我们使用多个线程访问同一资源的时候, 且多个线程中对资源有写的操作, 就容易出现线程安全问题.
要解决上述多线程并发访问一个资源的安全性问题: 也就是解决重复票与不存在票问题, Java 中提供了同步机制 (synchronized)来解决.
同步代码块.
同步方法.
锁机制.
1. 同步代码块
同步代码块: synchronized 关键字可以用于方法中的某个区块中, 表示只对这个区块的资源实行互斥访问.
格式:
synchronized(同步锁){ 需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念, 可以想象为在对象上标记了一个锁.
锁对象 可以是任意类型.
多个线程对象 要使用同一把锁.
- package synchroized_demo;
- /**
- * @author lx
- * @date 2019/1/22 - 16:49
- */
- public class Test_Syn {
- public static void main(String[] args) {
- // 创建线程对象 target
- Synchronized_demo synchronized_demo=new Synchronized_demo();
- // 创建三个售票窗口的线程
- Thread thread1 = new Thread(synchronized_demo,"窗口 1");
- Thread thread2 = new Thread(synchronized_demo,"窗口 2");
- Thread thread3 = new Thread(synchronized_demo,"窗口 3");
- thread1.start();
- thread2.start();
- thread3.start();
- }
- }
- package synchroized_demo;
- /**
- * @author lx
- * @date 2019/1/22 - 16:48
- */
- public class Synchronized_demo implements Runnable {
- /*
- * 创建共享资源, 只创建一个对象
- */
- private int tickets = 100;
- // 创建锁对象
- Object obj = new Object();
- // 重写 run 方法
- @Override
- public void run() {
- // 循环遍历出票
- while (tickets>0) {
- synchronized (obj) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 必须要在这里加上一个判断来控制票面不大于 0 的情况
- if (tickets!=0) {
- // 获取当前线程对象的名字并打印
- System.out.println(Thread.currentThread().getName() + "正在卖" + (tickets--) + "张票");
- }
- }
- }
- }
- }
同步方法
同步方法: 使用 synchronized 修饰的方法, 就叫做同步方法, 保证 A 线程执行该方法的时候, 其他线程只能在方法外等着.
- public synchronized void method(){
- // 可能会出现线程安全的代码
- }
同步锁:
1. 针对非 static 方法, 同步锁就是 this.
2. 针对 static 方法, 我们使用当前方法所在类的字节码对象(类名. class).
- package synchroized_demo;
- /**
- * @author lx
- * @date 2019/1/22 - 21:31
- */
- public class SvnPublic implements Runnable {
- private int tickets = 100;
- @Override
- public void run() {
- while (tickets> 0) {
- sellTicket();
- }
- }
- public synchronized void sellTicket() {
- if (tickets> 0) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName() + "正在出售" + (tickets--) + "张票");
- }
- }
- }
测试类
- package synchroized_demo;
- public class Test_synchronized_public {
- public static void main(String[] args) {
- SvnPublic svnPublic = new SvnPublic();
- Thread thread1 = new Thread(svnPublic, "售票口 1");
- Thread thread2 = new Thread(svnPublic, "售票口 2");
- Thread thread3 = new Thread(svnPublic, "售票口 3");
- thread1.start();
- thread2.start();
- thread3.start();
- }
- }
Lock 锁
public void lock() : 加同步锁.
public void unlock() : 释放同步锁.
- package lock;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
- /**
- * @author lx
- * @date 2019/1/22 - 22:28
- * public void lock() : 加同步锁.
- * public void unlock() : 释放同步锁.
- */
- public class LockDemo implements Runnable {
- private int tickets = 100;
- Lock lock = new ReentrantLock();
- /*
- * 执行卖票操作
- */
- @Override
- public void run() {
- // 每个窗口卖票的操作
- // 窗口 永远开启
- while (true) {// 有票 可以卖
- // 出票操作
- if (tickets> 0) {
- lock.lock();
- try {// 使用 sleep 模拟一下出票时间
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- if (tickets != 0) {
- System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "位置");
- lock.unlock();
- }
- }
- }
- }
- }
测试类
- package lock;
- public class Test {
- public static void main(String [] args){
- LockDemo lockDemo=new LockDemo();
- Thread t1=new Thread(lockDemo,"窗口 1");
- Thread t2=new Thread(lockDemo,"窗口 2");
- Thread t3=new Thread(lockDemo,"窗口 3");
- t1.start();
- t2.start();
- t3.start();
- }
- }
线程状态
当线程被创建并启动以后, 它既不是一启动就进入了执行状态, 也不是一直处于执行状态.
线程状态 导致状态发生条件
NEW(新建) 线程刚被创建, 但是并未启动. 还没调用 start 方法.
Runnable(可运行)
线程可以在 java 虚拟机中运行的状态, 可能正在运行自己代码, 也可能没有, 这取决于操作系统处理器.
Blocked(锁 阻塞)
当一个线程试图获取一个对象锁, 而该对象锁被其他的线程持有, 则该线程进入 Blocked 状 态; 当该线程持有锁时, 该线程将变成 Runnable 状态.
Waiting(无限 等待)
一个线程在等待另一个线程执行一个 (唤醒) 动作时, 该线程进入 Waiting 状态. 进入这个状态后是不能自动唤醒的, 必须等待另一个线程调用 notify 或者 notifyAll 方法才能够唤醒.
Timed Waiting(计时 等待)
同 waiting 状态, 有几个方法有超时参数, 调用他们将进入 Timed Waiting 状态. 这一状态 将一直保持到超时期满或者接收到唤醒通知. 带有超时参数的
常用方法有 Thread.sleep , Object.wait.
Teminated(被 终止)
因为 run 方法正常退出而死亡, 或者因为没有捕获的异常终止了 run 方法而死亡.
Timed Waiting(计时等待)
一个正在限时等待另一个线程执行一个 (唤醒) 动作的线程处于这一状态.
案例
实现一个计数器, 计数到 100, 在每个数字之间暂停 1 秒, 每隔 10 个数字输出一个字符串.
- /**
- * @author lx
- * @date 2019/1/23 - 10:23
- * 需求: 实现一个计数器, 计数到 100, 在每个数字之间暂停 1 秒, 每隔 10 个数字输出一个字符串
- * 分析: 有两种实方式: 1. 继承 Thread 类(类中也是实现了 Runnable 接口的).2. 直接实现 Runnable 接口
- * 这里采用继承 Thread 类, 并重写 run 方法
- * 遍历 0-100 的整数如果 i%10==0 输出一个字符串
- * 其他则输出连续无换行 i
- * 同时延时 1000 毫秒使用 Thread.sleep()方法
- */
- public class MyThread extends Thread {
- public static void main(String[] args) {
- new MyThread().start();
- }
- @Override
- public void run() {
- for (int i = 0; i < 100; i++) {
- if ((i % 10) == 0) {
- System.out.println("------" + i);
- }
- System.out.print(i);
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法, 单独的线程也可以调用, 不一定非要有协作关系.
为了让其他线程有机会执行, 可以将 Thread.sleep()的调用放线程 run()之内. 这样才能保证该线程执行过程中会睡眠.
sleep 与锁无关, 线程睡眠到期自动苏醒, 并返回到 Runnable(可运行)状态.
小提示: sleep()中指定的时间是线程不会运行的最短时间. 因此, sleep()方法不能保证该线程睡眠到期后就开始立刻执行.
Waiting(无限等待)
一个正在无限期等待另一个线程执行一个特别的 (唤醒) 动作的线程处于这一状态.
- package wait_Thread;
- public class WaitingTest {
- public static Object obj=new Object();
- public static void main(String[] args) {
- new Thread (new Runnable(){
- @Override
- public void run() {
- while (true){
- synchronized(obj){
- try {
- System.out.println(Thread.currentThread().getName()
- +"=== 获取到锁对象, 调用 wait 方法, 进入 waiting 状态, 释放锁对象");
- obj.wait();// 无限等待
- // obj.wait(5000); // 计时等待, 5 秒 时间到, 自动醒来
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println( Thread.currentThread().getName()+
- "=== 从 waiting 状态醒来, 获取到锁对象, 继续执行了");
- }
- }
- }
- },"等待唤醒").start();
- new Thread(new Runnable() {
- @Override
- public void run() {
- // while (true){
- try {
- System.out.println(Thread.currentThread().getName()+"等待 3 秒");
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- synchronized (obj){
- System.out.println(Thread.currentThread().getName()+"获取到锁对象, 调用 notify 方法, 唤醒线程");
- obj.notify();
- }
- }
- },"唤醒线程").start();
- }
- }
来源: http://www.jianshu.com/p/2b5bb7d31744