前言
一个应用程序可能有多个进程,一个进程可能有多个线程,多线程并非同时进行,而是充分利用cpu的资源,因为其切换时间很短,所以直观上认为是并行的。
创建线程的两种方式
不管是哪种方式,都需要重写run方法,所有的业务处理都在这里
继承Thread
自定义线程类:
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
super(name); //调用父类的String参数的构造方法,指定线程的名称
}
//重写run方法,完成该线程执行的逻辑
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
测试类
public class Demo01 {
public static void main(String[] args) {
MyThread mt = new MyThread("新的线程!"); //创建自定义线程对象
mt.start(); //开启新线程
for (int i = 0; i < 10; i++) {//在主方法中执行for循环
System.out.println("main线程!"+i);
}
}
}
上面可以发现,会分别打印main线程!和新的线程!他们是分开在不同线程中执行的。
实现Runnable
自定义线程类:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
测试类:
public class Demo {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "新的线程");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("主线程" + i);
}
}
}
跟继承Thread基本一致,只是写法不同,其中也是有一些区别的。
继承Thread 和实现Runnable的区别
应该说实现Runnable更加有优势,具体大概如下:
- 实现Runnable,可以避免java中的单继承的局限性
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类
匿名内部线程
有时候只需要新起一个线程,处理一些事情,不需要被调用,可以直接新建内部线程,方便快捷。
public class NoNameInnerClassThread {
public static void main(String[] args) {
new Thread() {
public void run() {
for(int i=0;i<10;i++) {
System.out.println("aaaaaaa");
}
}
}.start();
new Thread(new Runnable() {
public void run() {
for(int i=0;i<10;i++) {
System.out.println("bbbbbbb");
}
}
}).start();
}
}
线程安全
多线程面临的一个问题就是,多个线程,同时共享一个数据的情况下,并且同时对其存在写的操作,就存在线程一取到数据,还没来得及更新写,线程二就取到了老数据,并对其进行处理,最典型的就是卖票,一百个票,线程一拿到的总数是100,并准备卖,还没写入数据库100-1.这时候线程二又拿到100,并减1写入数据库,相当于第100张票被卖了两次,这肯定是不行的。
卖票代码:
public class Ticket implements Runnable{
private int ticket = 100;
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
if (ticket > 0) {//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
执行代码
public class TestTicket1 {
public static void main(String[] args) {
// 创建线程任务对象
Ticket ticket = new Ticket();
// 创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
// 同时卖票
t1.start();
t2.start();
t3.start();
}
}
解决线程安全的三种方法
线程同步
主要是使用synchronized关键字,当一个线程正在对其进行操作的时候,其他线程无法进入。最多允许一个线程拥有同步锁,修改后的代码如下
public class Ticket2 implements Runnable{
private int ticket = 100;
Object lock = new Object();
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
synchronized (lock) {
if (ticket > 0) {//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
}
锁对象 可以是任意类型,String lock = new String();也可以
方法同步
synchronized 修饰的方法保证A线程执行该方法的时候,其他线程只能在方法外等着。
改造后的代码如下:
public class Ticket3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
// 每个窗口卖票的操作
// 窗口 永远开启
while (true) {
sellTicket();
}
}
public synchronized void sellTicket() {
if (ticket > 0) {// 有票 可以卖
// 出票操作
// 使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
// 获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
Lock锁
Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。其使用方法更加方便,在需要加锁的代码前加上锁,直到不需要解锁的地方解锁,代码如下:
public class Ticket4 implements Runnable {
private int ticket = 100;
Lock lock = new ReentrantLock();
@Override
public void run() {
// 每个窗口卖票的操作
// 窗口 永远开启
while (true) {
lock.lock();
if (ticket > 0) {// 有票 可以卖
// 出票操作
// 使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
// 获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
lock.unlock();
}
}
}
线程状态
借用网上的一张图:
项目 | Value |
---|---|
New | 线程对象被创建后,就进入了新建状态 |
Runnable | 就绪状态,代码调用.start()方法即进入。 |
Running | 运行状态,线程获得cpu使用权,进入运行状态 |
Blocked | 阻塞状态,变为阻塞状态有几种情况,1、线程调用wait方法进入。2、线程获取synchronized同步锁失败进入,因为其他线程在使用同步锁。3、线程调用sleep()或join()或发出了I/O请求时进入。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 |
Dead | 死亡状态,线程执行完毕或异常退出run()方法,生命周期结束。 |
线程间通信
上面说到wait/notify方法,一个是让线程等待,一个是唤醒正在沉睡的线程,为什么会有这玩意?因为线程间需要通信,比如吃米饭的和做米饭的,分别是两个动作,分别在两个线程中执行,即生产者和消费者之间的关系,其二者需要共享一个变量,判断是都有米饭,如果有,则唤醒吃米饭线程来吃,做米饭线程等待,如果没有,则唤醒做米饭线程来做,吃米饭线程等待。
线程池
新建一个线程,执行完以后销毁,这样没问题,但是频繁的创建线程很消耗资源,所以出现了线程池,需要线程的时候,去线程池拿,用完再放回去,避免频繁创建新的线程。
创建线程池:
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
获取线程池中的线程,并执行run方法
service.submit(r);//获取线程并执run,submit都执行了
如果线程池不需要了可以用service.shutdown();关闭