多线程
程序、进程、线程
线程的创建和使用
程序:一段静态的代码
进程:是程序的一次执行过程。或是正在运行的一个程序。是一个动态的过程
进程作为资源分配的单位
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器
一个线程中的多个线程共享相同的内存单元/内存地址空间–>他们从同一堆中分配对象,可以访问相同的变量和对象。使得线程间通信更简洁,高效。但多线程操作可能会带来安全隐患
一、多线程的创建:方式一:继承于Thread类
1.创建一个继承于Thread类的子类
2.重写Thread类的run() 方法
3.创建Thread类的子类的对象
4.通过此对象调用start()
例子:遍历100以内的所有的偶数
//1.创建一个继承于Thread类的子类
class MyThread extends Thread {
//2.重写Thread类的run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" +i); //获得当前的线程名
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3.创建Thread类的子类的对象
MyThread myThread = new MyThread();
//4.通过此对象调用start() 作用:①启动当前线程 ②调用当前线程的run()
myThread.start();
//问题一:我们不能通过直接调用run()的方式启动线程
//myThread.run();//这样根本就没有启动线程,还是main的线程,这里还是体现的是对象调方法
//问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行,会报IllegalThreadException
//myThread.start();
//需要重新创建一个线程的对象
MyThread myThread2 = new MyThread();
myThread2.start();
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" +i); //获得当前的线程名
}
}
}
}
另一种简单写法,创建Thread的匿名子类匿名对象
public class ThreadTest {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
}
}.start();
}
}
总结:要想创建多个线程就要创建多个对象,要想启动线程就要通过start()方法。
二、线程的常用方法
1.start():启动当前线程:调用当前线程的run()方法
2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明再此方法中
3.currentThread():静态方法,返回执行当前代码的线程
4.getName():获取当前线程的名字
5.setName():设置当前线程的名字 //给主线程改名:Thread.currentThread().setName(“主线程”);
//也可以通过构造器个线程命名,在子类中写一个带参的构造器,调用父类带参的构造器
6.yield():释放当前cpu的执行权(但是在下一刻当前线程可能又会被分配到执行权)
7.join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
8.stop():已过时。当执行此方法时,强制结束当前线程
9.sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态
10.isAlive():判断当前线程是否存活
11.getPriority():获取线程的优先级
12.setPriority(int p):设置线程的优先级
13.线程的优先级:
**说明:*高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。 并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
/**
* 测试Thread中的常用方法
* @author ZAQ
* @create 2020-02-05 14:21
*/
class HelloThread extends Thread {
public HelloThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) {
//sleep()测试
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + getPriority() + ": " + i);
}
//yield()测试
if(i % 20 == 0) {
yield();
}
}
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
HelloThread h1 = new HelloThread("Thread:1");
//设置分线程的优先级
h1.setPriority(Thread.MAX_PRIORITY);
h1.start();
//给主线程命名
Thread.currentThread().setName("主线程");
//设置主线程的优先级
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ":" + getPriority() + ": " + i);
}
//join()测试
if(i == 20) {
try {
h1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//isAlive()测试
System.out.println(h1.isAlive());
}
}
三、例题:创建3个窗口买票,票数为100,使用继承Thread的方式
**
* 创建3个窗口买票,票数为100
* 存在线程安全问题,待解决。
* @author ZAQ
* @create 2020-02-05 16:37
*/
class Window extends Thread {
private static int ticket = 100; //每个对象共享同一个变量,不设置为static的将会有300张票
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket--);
} else {
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window window1 = new Window();
Window window2 = new Window();
Window window3 = new Window();
window1.setName("Window1");
window2.setName("Window2");
window3.setName("Window3");
window1.start();
window2.start();
window3.start();
}
}
四、多线程的创建:方式二:实现Runable接口
1.创建一个实现了Runnable接口
2.实现类去实现Runnable中的抽象类方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()
/**
* 多线程的创建:方式二:实现Runable接口
*
* @author ZAQ
* @create 2020-02-05 17:01
*/
//1. 创建一个实现了Runnable接口
class MThread implements Runnable {
// 2. 实现类去实现Runnable中的抽象类方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3. 创建实现类的对象
MThread mThread = new MThread();
//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread thread = new Thread(mThread);
//5. 通过Thread类的对象调用start() ①:启动线程 ②:调用当前线程run()方法 --> 调用了Runnable类型的target的run()方法
thread.start();//通过源码可知:这里Thread里的run()方法就是调用target的run()方法,而生成对象初始化时将传进来的mThread赋给了targer,
// 所以这里通过thread调用的start方法调用的run()方法就是调用MThread重写的run()方法
//在启动一个线程,遍历100以内的偶数
Thread t2 = new Thread(mThread);
t2.start();
}
}
另一种简单写法:匿名的方式实现Runnable接口
public class ThreadTest {
public static void main(String[] args) {
new Thread(new Runnable() {/*Runnable实现类的匿名对象对象*/
@Override
public void run() {
}
}).start();
}
}
五、例题:创建3个窗口买票,票数为100,使用实现Runnable的方式
/**
* 存在线程安全问题,暂不考虑
* @author ZAQ
* @create 2020-02-05 17:37
*/
class Windows implements Runnable {
private int ticket = 100; //不加static也只有100张票,原因看下面注释
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket--);
} else {
break;
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Windows w = new Windows(); //这是因为这里都共用一个对象
Thread thread1 = new Thread(w);
Thread thread2 = new Thread(w);
Thread thread3 = new Thread(w);
thread1.setName("Windows1");
thread2.setName("Windows2");
thread3.setName("Windows3");
thread1.start();
thread2.start();
thread3.start();
}
}
六、两种创建方式的对比
开发中:优先选择:实现Runable接口的方式
原因:
1.实现的方式没有类的单继承的局限性(继承的Thread方式将没有办法继承其他类,实现的方式还可继承其他类,或其他方法)
2.实现的方式更合适来处理多个线程有共享数据的情况
联系:Thread也是实现Runnable接口
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
线程的生命周期
新建:当一个Thread类或其子类的对象被声明或创建时,新生的线程对象处于创建状态
就绪:处于创建状态的线程被start()后,将进入线程队列等待CPU时间片,此时他已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
死亡:线程完成了他的全部工作或线程被提前强制地终止或出现异常导致结束
线程的同步
问题提出
多个线程执行的不确定性引起执行结果的不确定
多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
解决线程安全问题的方式一、二
例子:创建三个窗口卖票,总数为100张,使用实现Runnable接口的方法。
在Java中,我们通过同步机制,来解决线程的安全问题
方式一:同步代码块
synchronized(同步监视器) {
//需要被同步的代码
}
说明:
① 操作共享的数据的代码,即为需要被同步的代码
② 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
③ 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用用一把锁。
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的
说明:
① 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
② 非静态的同步方法,同步监视器:this
静态的同步方法,同步监视器是:当前类本身
同步的方式,解决了线程的安全问题。 ------好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。------局限性
实例一:使用同步代码块的方式解决实现Runnable接口的线程安全问题
/**
* 1.问题:卖票的过程中,出现了重票、错票(0、-1) --> 出现了线程的安全问题
* 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
* 3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来,直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
* @author ZAQ
* @create 2020-02-06 16:16
*/
class Windows implements Runnable {
private int ticket = 100;
Object obj = new Object();//创建一把锁
@Override
public void run() {
//Object obj = new Object(); 锁不能在这里声明,这里就相当于创建了三把锁
while (true) {
synchronized(obj) {//也不能在这里new Object() 使用this是可以的
if (ticket > 0) {
try {
Thread.sleep(100);//一个线程进入,在这里睡着了。其他的线程这时就会进入,产生安全问题
//注意:这里就是不休眠也存在安全问题,只是概率比较低,在这里执行休眠,只是让这个概率提升了
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket--);
} else {
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Windows w = new Windows();
Thread thread1 = new Thread(w);
Thread thread2 = new Thread(w);
Thread thread3 = new Thread(w);
thread1.setName("Windows1");
thread2.setName("Windows2");
thread3.setName("Windows3");
thread1.start();
thread2.start();
thread3.start();
}
}
实例二:使用同步代码块的方式解决继承Thread的线程安全问题
说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
// An highlighted block
/**
* 使用同步代码块的方式解决继承Thread的线程安全问题
*说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视 器。
* @author ZAQ
* @create 2020-02-06 16:59
*/
class Windows2 extends Thread {
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) { //正确的方式
//synchronized (Windows2.class) {正确的方式二:Class clazz = Windows2.class。Windows2.class只会加载一次!
//synchronized (this) { //错误的方式:this代表着w1,w2,w3三个对象,三把锁
if(ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ": " + ticket--);
} else {
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Windows2 w1 = new Windows2();
Windows2 w2 = new Windows2();
Windows2 w3 = new Windows2();
w1.setName("窗口一:");
w2.setName("窗口二:");
w3.setName("窗口三:");
w1.start();
w2.start();
w3.start();
}
}
实例三:使用同步方法解决实现Runnable接口的线程安全问题
/**
* 使用同步方法解决实现Runnable接口的线程安全问题
*
* @author ZAQ
* @create 2020-02-06 16:52
*/
class Windows3 implements Runnable {
private int ticket = 100;
Object obj = new Object();
boolean flag = true;
@Override
public void run() {
while (flag) {
show();
}
}
private synchronized void show() { //同步监视器:this。因为this是唯一的
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket--);
} else {
flag = false;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Windows3 w = new Windows3();
Thread thread1 = new Thread(w);
Thread thread2 = new Thread(w);
Thread thread3 = new Thread(w);
thread1.setName("Windows1");
thread2.setName("Windows2");
thread3.setName("Windows3");
thread1.start();
thread2.start();
thread3.start();
}
}
实例四:使用同步方法的方式解决继承Thread的线程安全问题
/**
* @author ZAQ
* @create 2020-02-06 17:43
*/
class Windows4 extends Thread {
private static int ticket = 100;
private static boolean flag = true;
@Override
public void run() {
while (flag) {
show();
}
}
private static synchronized void show() {//同步监视器:Windows4.class
//private synchronized void show() {//同步监视器:w1,w2,w3。有三个锁,此解决方式是错的
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticket--);
} else {
flag = false;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Windows4 w1 = new Windows4();
Windows4 w2 = new Windows4();
Windows4 w3 = new Windows4();
w1.setName("窗口一:");
w2.setName("窗口二:");
w3.setName("窗口三:");
w1.start();
w2.start();
w3.start();
}
}
实例五:线程安全的单例模式——懒汉式
/**
* 线程安全的懒汉式
* @author ZAQ
* @create 2020-02-06 19:21
*/
public class BankTest {
public static void main(String[] args) {
}
}
class Bank {
private Bank() {
}
private static Bank bank;
public static Bank getBank() {
//方式一:效率稍差(不管有没有给bank实例化上对象,进来的线程都需要等待)
// synchronized (Bank.class) {
// if(bank == null) {
// bank = new Bank();
// }
// return bank;
// }
//方式二:效率更高
if(bank == null) {
synchronized (Bank.class) {
if(bank == null) {
bank = new Bank();
}
}
}
return bank;
}
}
解决线程安全问题的方式三:Lock锁—JDK5.0新增
1.实例化ReentrantLock
2.在try代码块中调用上锁方法lock()
3.在finally代码块中调用解锁方法unlock()
实例六:
/**
* @author ZAQ
* @create 2020-02-06 21:04
*/
class Lock implements Runnable{
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
lock.lock();
while (true) {
if(ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticket--);
} else {
break;
}
}
} finally {
lock.unlock();
}
}
}
public class LockTest {
public static void main(String[] args) {
Lock lock = new Lock();
Thread thread1 = new Thread(lock);
Thread thread2 = new Thread(lock);
Thread thread3 = new Thread(lock);
thread1.setName("窗口一");
thread2.setName("窗口二");
thread3.setName("窗口三");
thread1.start();
thread2.start();
thread3.start();
}
}
线程的死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
线程的通信
涉及到三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器(锁)。
notify():一旦执行此方法,就会唤醒wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个线程。
notifyAll():一旦执行此方法,就会唤醒wait的所有线程。
说明:
wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中。
wait()、notify()、notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现ILLegalMonitorStateException异常。
wait()、notify()、notifyAll()三个方法定义在java.lang.Object类中
例子:使用两个线程打印 1-100。线程1,线程2 交替打印
/**
* @author ZAQ
* @create 2020-02-06 22:36
*/
class Number implements Runnable {
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
obj.notify();
if (number <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + number++);
} else {
break;
}
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread thread1 = new Thread(number);
Thread thread2 = new Thread(number);
thread1.setName("线程一");
thread2.setName("线程二");
thread1.start();
thread2.start();
}
}
JDK5.0新增线程创建方式
一、多线程的创建:方式三:实现Callable接口
与使用Runable相比,Callable功能更强大些
相比run()方法,可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
解决线程安全问题的方式一、二
创建过程
1.创建一个实现Callable的实现类
2.实现call方法,将此线程需要执行的操作声明在Call()中
3.创建Callable接口实现类的对象
4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
6.获取Callable中的call方法的返回值 (可有可无get(),根据实际情况,看需不需要call的返回值)
/**
* @author ZAQ
* @create 2020-02-07 12:43
*/
//1. 创建一个实现Callable的实现类
class NumThread implements Callable<Integer> {
// 2. 实现call方法,将此线程需要执行的操作声明在Call()中
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i%2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
// 3. 创建Callable接口实现类的对象
NumThread numThread = new NumThread();
// 4. 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask<Integer> futureTask = new FutureTask<>(numThread);
// 5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
Thread thread = new Thread(futureTask);
thread.start();
try {
// 6. 获取Callable中的call方法的返回值 (可有可无get(),根据实际情况,看需不需要call的返回值)
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
Integer sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
call()可以有返回值。
call()可以抛出异常,被外面的操作捕获,获取异常的信息。run()方法不能抛出异常
Callable是支持泛型的。
二、多线程的创建:方式四:使用线程池(开发中常用)
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
没有线程池就相当于:如果我要去天安门广场,需要一个交通工具,自行车,这时我就需要自己造一辆自行车,到了天安门以后,再将自行车销毁。如果有了线程池,就相当于共享单车。自行车已将造好了,如果我需要的的时候只需要去拿就行了,用完了再放回去。
使用线程池的好处:
提高了响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程, 不需要每次都创建)
便于线程管理(如果每个人都自己造一个线程将会产生拥堵)
corePoolSize:线程池的大小
maximumPoolSize:最大线程数
/**
* @author ZAQ
* @create 2020-02-07 13:41
*/
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0; i < 100; i++) {
if(i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0; i < 100; i++) {
if(i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
service1.setCorePoolSize(2);
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
//提供两个线程完成不同的操作
service1.execute(new NumberThread()); //适合使用于Runnable
service1.execute(new NumberThread1()); //适合使用于Runnable
//service.submit(Callable callable); //适合使用于Callable
//3.关闭连接池
service1.shutdown();
}
}