创建线程的三种方式
方式一:继承Thread类
实现步骤:
-
继承Thread类并重写run()方法;
-
创建线程并启动。
代码实现:
public class MyThread extends Thread {@Overridepublic void run() {for(int i=0; i<100; i++) {System.out.println(i);}}
}
创建两个线程测试一下:
public class MyThreadDemo {public static void main(String[] args) {MyThread my1 = new MyThread();MyThread my2 = new MyThread();my1.start();my2.start();}
}
输出结果如下:
-
为什么要重写run()方法?
因为run()中封装被线程执行的代码。
方式二:实现Runnable接口
Thread构造方法
方法名 | 说明 |
---|---|
Thread(Runnable target) | 创建一个线程 |
Thread(Runnable target, String name) | 创建一个线程,线程名字为name |
实现步骤:
-
实现Runnable接口并重写run()方法,然后创建实现类的对象,表示线程要执行的任务;
-
把Runnable接口实现类的对象作为构造方法的参数创建一个线程并启动;
代码实现:
public class RunDemo1 implements Runnable{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread() + "" + i);}}
}
创建线程对象:
public class TestDemo2 {public static void main(String[] args) {RunDemo1 r = new RunDemo1();// 其中r表示线程要执行的任务Thread t1 = new Thread(r);Thread t2 = new Thread(r);t1.start();t2.start();}
}
方式三:实现Callable接口
由于前两种方式的run方法都是没有返回值的,第三种可以获取返回值。
方法介绍:
方法名 | 说明 |
---|---|
V call() | 计算结果,如果无法计算结果,则抛出一个异常 |
FutureTask(Callable<V> callable) | 创建一个 FutureTask,一旦运行就执行给定的 Callable |
V get() | 用来获取执行结果 |
实现步骤: 实现 Callable 接口,重写 call 方法,这种方式可以通过 FutureTask 获取任务执行的返回值。
代码实现:
public class MyCallable implements Callable {@Overridepublic Object call() throws Exception {return "nihao";}
}
测试一下:
public class TestDemo3 {public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable cl = new MyCallable();FutureTask<String> ft = new FutureTask<String>(cl);Thread t = new Thread(ft);t.start();System.out.println(ft.get());}
}
输出结果:
线程中常用的方法
设置和获取线程名称的方法
方法名 | 说明 |
---|---|
void setName(String name) | 将线程的名字设置为参数name |
String getName() | 返回此线程的名字 |
static Thread currentThread() | 返回执行到这行代码的线程 |
休眠线程方法
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程暂停指定的毫秒数 |
需要注意的是,sleep 的时候要对异常进行处理。
try {//sleep会发生异常要显示处理Thread.sleep(20);//暂停20毫秒
} catch (InterruptedException e) {e.printStackTrace();
}
线程优先级相关方法
方法名 | 说明 |
---|---|
final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改线程的优先级,线程默认优先级是5 |
如果获取main线程的优先级呢?
首先使用Thread.currentThread()获取到main线程对象;
然后再使用getPriority()获取优先级。
Thread类中与优先级有关的代码,可以看到线程优先级的范围是1-10。
// The minimum priority that a thread can have.public static final int MIN_PRIORITY = 1;// The default priority that is assigned to a thread.public static final int NORM_PRIORITY = 5;// The maximum priority that a thread can have.public static final int MAX_PRIORITY = 10;
数字越大表示优先级越高,cpu执行这个线程的概率越高,反之越低。
优先级只是概率问题,并不是绝对的。
下面举一个例子:
public class TestDemo1 {public static void main(String[] args) {ThreadDemo1 t1 = new ThreadDemo1("线程1:");ThreadDemo1 t2 = new ThreadDemo1("线程2:");t1.setPriority(1);t2.setPriority(10);t1.start();t2.start();}
}
程序的执行结果中是有可能优先级低的线程1先结束执行的。这里就不提供图片了。
守护线程方法
方法名 | 说明 |
---|---|
void setDaemon(boolean on) | 将此线程标记为守护线程,当非守护线程结束后守护线程也慢慢结束 |
ThreadDemo1中的run方法:
@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(this.getName() + ":" + i);}}
ThreadDemo2中的run方法:
@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(this.getName() + ":" + i);}}
创建两个线程,然后将线程t2设置为守护线程,代码如下:
public class TestDemo4 {public static void main(String[] args) {ThreadDemo1 t1 = new ThreadDemo1();ThreadDemo2 t2 = new ThreadDemo2();// 将线程t2设置为守护线程t2.setDaemon(true);t1.start();t2.start();}
}
可以看到在非守护线程t1结束之后,守护线程t2也慢慢结束了。
礼让线程方法
Java 中的优先级不是特别的可靠,Java 程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定会采纳。而真正的调用顺序,是由操作系统的线程调度算法来决定的。
插入线程方法
比如将线程t1插入到当前正在运行的main线程之前:
public class TestDemo5 {public static void main(String[] args) throws InterruptedException {ThreadDemo1 t1 = new ThreadDemo1();t1.start();t1.join();for (int i = 0; i < 40; i++) {System.out.println("main线程:" + i);}}
}
运行结果:
Java 线程的 6 个状态:
// Thread.State 源码
public enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}
线程的3个状态:就绪,运行,阻塞。图示如下:
cpu执行哪个线程是由操作系统进行调度的,有一些常见的线程调度算法,比如先来先服务,最短作业优先,最短剩余时间优先,轮转法等等。
synchronized
synchronized关键字用来为这段代码加一个锁,如果只是一部分代码,则可用同步代码块的方式加锁,锁需要手动提供,而如果是整个方法的代码,则可使用同步方法加锁,这种方式的锁是JVM自动提供的。
同步代码块
多线程并发执行可能会出现数据安全问题,比如下面的三个窗口卖100张票的例子。
刚开始代码是这样写的:
public class MyThread1 extends Thread{static int ticket = 0;@Overridepublic void run() {while (true) {if (ticket > 99) {break;} else {ticket++;System.out.println(getName() + "卖了第" + ticket + "张票");}}}
}
有两个问题:①相同的票;②超过范围的票。
下面是正确的代码,加锁的目的是为了使操作具有原子性,当锁没有释放时,别的线程是不能执行这段代码的,一般锁对象用static修饰保证唯一性。
public class MyThread1 extends Thread {static int ticket = 0;final static Object obj = new Object();@Overridepublic void run() {while (true) {synchronized (obj) {if (ticket > 99) {break;} else {ticket++;System.out.println(getName() + "卖了第" + ticket + "张票");}}}}
}
如果输出结果的顺序是混乱的,比如下面这种情况,是为什么呢?
因为锁没有起作用,比如定义创建一个锁的代码,因为在创建每一个新的线程的时候都会创建一个obj,锁不唯一。
final Object obj = new Object();
同步方法
用synchronized关键字修饰的方法。synchronized关键字写到访问修饰符的后面。
JVM为同步方法自动提供了一个锁:
①非静态方法的锁是调用同步方法的对象;
②静态方法的锁是字节码文件。
锁默认是打开的,当有一个线程进去之后,锁关闭,当代码执行完毕之后锁才打开。
线程在哪里?
执行这行代码的线程。
尝试将上面例子中同步代码块的代码抽取为一个同步方法,代码如下,但是不对,因为同步方法中的锁是调用此方法的对象,此时有三个锁,分别是3个线程,锁不唯一。
public class MyThread1 extends Thread {static int ticket = 0;final static Object obj = new Object();@Overridepublic void run() {while (true) {if (saleTicket()) break;}}private synchronized boolean saleTicket() {if (ticket > 99) {return true;} else {ticket++;System.out.println(getName() + "卖了第" + ticket + "张票");}return false;}
}
所以同步方法一般使用Runnable接口,因为这种情况下锁唯一。
public class MyRunnable implements Runnable{static int ticket = 0;final static Object obj = new Object();@Overridepublic void run() {while (true) {if (saleTicket()) break;}}private synchronized boolean saleTicket() {if (ticket > 99) {return true;} else {ticket++;System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");}return false;}
}
Lock锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。Lock是接口,不能直接实例化,这里采用它的实现类ReentrantLock来实例化。
-
ReentrantLock构造方法
方法名 说明 ReentrantLock() 创建一个ReentrantLock的实例 -
加锁解锁方法
方法名 说明 void lock() 获得锁 void unlock() 释放锁
代码如下,有两个细节:
①锁用static修饰,唯一性。
②当某个线程进入锁时,由于ticket < 100为false,执行else里的break结束while循环,但是并没有释放锁,所以程序无法结束,可以将lock.unlock();放到finally子句中确保锁一定会释放。
有一个疑问:用synchronized的锁是什么释放的?
在大括号结束时释放的。
public class MyThread1 extends Thread {static int ticket = 0;final static Lock lock = new ReentrantLock();@Overridepublic void run() {while (true) {lock.lock();try {if (ticket > 99) {break;} else {ticket++;System.out.println(getName() + "卖了第" + ticket + "张票");}} finally {lock.unlock();}}}
}
子父类有关异常的处理?
生产者消费者模式(等待唤醒机制)
由于线程之间是抢占式执⾏的,因此线程之间执⾏的先后顺序难以预知。但是实际开发中希望协调多个线程的执行顺序,实现线程间的协作。
在 Java 中可以用 wait、notify 和 notifyAll 来实现线程间的通信。
wait和notify两个方法被提取到顶级父类Object中。Object类的等待和唤醒方法:
方法名 | 说明 |
---|---|
void wait() | 让当前线程进⼊等待状态,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 |
void notify() | 随机唤醒等待队列中的一个线程 |
void notifyAll() | 唤醒等待队列中的所有线程 |
线程在运行的时候,如果发现某些条件没有被满足,可以调用wait方法放弃已经获得的锁,并且暂停自己的执行,然后进入等待状态。
当该线程被其他线程唤醒并获得锁后,可以沿着之前暂停的地方继续向后执行,而不是再次从同步代码块开始的地方开始执行。
但是需要注意的一点是,在线程通信中涉及到条件判断时要用while而不是if。因为while语句来说,这样在线程被唤醒后,会再次判断条件是否正真满足。
信号量实现
public class Food {static boolean flag;public synchronized static void eat() {while (!flag){try {Food.class.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("小张正在吃汉堡");flag = !flag;Food.class.notifyAll();}public synchronized static void cook() {while (flag){try {Food.class.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("小刘正在做法式大餐");flag = !flag;Food.class.notifyAll();}
}
public class Consumer extends Thread{@Overridepublic void run() {for (int i = 0; i < 100; i++) {Food.eat();}}
}
public class Producer extends Thread{@Overridepublic void run() {for (int i = 0; i < 100; i++) {Food.cook();}}
}
阻塞队列实现
即生产者和消费者中间有一个缓冲区或者叫容器用来存放产品,可以存放多个。
(1)当CPU执行到生产者这个线程时:
①首先判断容器里产品是否已满;
②已满就调用wait方法使自己进入等待状态;
③没有则生产,使产品数量+1,并唤醒等待的消费者进行消费。
关于其中的notifyAll:由于没有产品可消费了,消费者处于等待状态,当生产者生产了产品后,唤醒消费者。
(2)当CPU执行到消费者这个线程时:
①首先判断容器里是否有产品;
②没有就调用wait方法使自己进入等待状态;
③有则消费,使产品数量-1,并唤醒等待的生产者进行生产。
实现如下:
产品描述Product类代码如下:
public class Product {int id;public Product(int id) {this.id = id;}
}
用来装产品的容器类SynContainer代码如下:
public class SynContainer {Product[] products = new Product[10];// 既表示产品的个数,也表示生产的产品存入数组中的下标int count = 0;public synchronized void put(Product p) {// 判断容器是否满了// 1. 满了while (count == products.length) {try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}products[count] = p;count++;this.notifyAll();}public synchronized Product take() {// 首先判断容器里是否有产品// 1. 没有if (count == 0) {try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}count--;this.notifyAll();return products[count];}
}
生产者Producer的代码如下:
public class Producer extends Thread{SynContainer synContainer;public Producer(SynContainer synContainer) {this.synContainer = synContainer;}@Overridepublic void run() {for (int i = 1; i < 100; i++) {synContainer.put(new Product(i));System.out.println("生产者生产了第" + i + "个产品");}}
}
消费者Consumer代码如下:
public class Consumer extends Thread{SynContainer synContainer;public Consumer(SynContainer synContainer) {this.synContainer = synContainer;}@Overridepublic void run() {for (int i = 1; i < 100; i++) {System.out.println("消费者消费了第" + synContainer.take().id + "个产品");}}
}
测试类代码如下:
public class Test {public static void main(String[] args) {SynContainer synContainer = new SynContainer();Producer producer = new Producer(synContainer);Consumer consumer = new Consumer(synContainer);producer.start();consumer.start();}
}
由于线程中的run方法并没有使用锁,因此输出的时候顺序会乱,如下:
还有一个细节,如果如何确保生产者和消费者使用的是同一个容器,即在Producer和Consumer类中只声明一个SynContainer变量,由各自的构造方法传递实际的容器类对象。