线程的概念:
一个线程是指程序中完成一个任务的执行流,java中可以在一个程序中并发地运行多个线程,这些线程可以同时在多个处理器上运行
在单CPU系统中,多个线程分享CPU的时间,操作系统负责CPU资源的调度和分配
多线程可以使程序的反应更快,交互性更强,执行效率更高。当程序作为一个应用程序(application)运行时,jvm会为main方法创建一个线程,当程序不再需要时,jvm就会创建一个线程来进行垃圾回收的工作。所以一个完整的应用程序最少含有两个线程。
2. 创建线程
2.1 继承父类Thread
- 将类声明为Thread的子类
- 重写Thread类的run()方法
- 创建Thread的子类对象,并且调用start()方法,启动线程
public class MyThread extends Thread {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
for(int i=0;i<100;i++){
System.out.println("main-->"+i);
}
}
public void run() {
for(int i=0;i<100;i++){
System.out.println("自定义-->"+i);
}
}
}
2.2 实现Runnable接口
- 创建类并实现Runnable接口
- 实现Runnable接口里的run方法,完成自定义线程的任务代码
- 实例化类
- 创建Thread对象,参考Thread构造方法:Thread(Runnable target)
- 调用start()方法,启动线程
public class MyThread3 implements Runnable {
public void run() {
for(int i=1;i<100;i++){
System.out.println(Thread.currentThread()+":"+i);
}
}
public static void main(String[] args) {
//创建类实例
MyThread3 thread3 = new MyThread3();
//创建Thread类的实例,把Runnable作为实参传递
Thread thread = new Thread(thread3,"线程-C");
//调用thread的start()方法启动线程
thread.start();
for(int i=1;i<100;i++){
System.out.println(Thread.currentThread()+":"+i);
}
}
}
Note:因为java是单继承、多实现的,所以建议使用实现Runnable接口的方法来实现线程
3. 线程常见的方法
- 设置线程的名字
可以使用带参数的构造方法Thread(String name) 或者thread.setName(“myThread”)方法,为线程命名。同样getName()可以得到线程的名字。 - 睡眠线程
注意:sleep是静态方法,所以哪一个线程执行了含有sleep方法的代码,哪一个线程就会睡眠
考虑以下代码是哪一个线程睡眠了?
public class MyThread2 extends Thread {
//自定义线程
public void run() {
for(int i=0;i<100;i++){
System.out.println(i);
}
}
//主线程
public static void main(String[] args) throws InterruptedException {
MyThread2 thread2 = new MyThread2();
thread2.sleep(5 * 1000);
thread2.setName("线程-A");
thread2.start();
}
}
“thread2.sleep(5 * 1000);”是在主线程中执行,所以主线程会被睡眠
public class MyThread2 extends Thread {
//自定义线程
public void run() {
try {
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<100;i++){
System.out.println(i);
}
}
//主线程
public static void main(String[] args) throws InterruptedException {
MyThread2 thread2 = new MyThread2();
thread2.start();
}
}
“Thread.sleep(2 * 1000);”是在自定义线程中执行,所以自定义线程会被睡眠
3. 得到当前线程对象
静态方法,哪个线程执行了含有currentThread()的代码,就返回哪个线程的对象
public class MyThread2 extends Thread {
public MyThread2(String name) {
super(name);
}
//自定义线程
public void run() {
System.out.println("this:"+this);
System.out.println("2:"+Thread.currentThread());
}
//主线程
public static void main(String[] args) throws InterruptedException {
MyThread2 thread2 = new MyThread2("线程-B");
thread2.start();
System.out.println("1:"+thread2.currentThread());
}
}
运行后控制台输出如下:
4. 设置线程的优先级
1:Thread[main,5,main]
this:Thread[线程-B,5,main]
2:Thread[线程-B,5,main]
优先级越高的线程获取CPU资源的几率越大
//设置优先级1~10之间,数值越大优先级越高,默认的优先级为5
thread2.setPriority(10);
优先级越高的线程不会100%优先执行,只是优先执行的概率更大而已
5. isAlive()
isAlive()是用来判断线程的状态,当线程处于就绪、临时阻塞、运行状态,则返回true;如果线程处于新建并且没有启动的状态,或者线程已经执行结束,则返回false.
4. 线程的生命周期
- 新建状态:new Thread()后,线程就进入了新建状态
- 调用start()方法启动线程后,线程就会进入可运行状态<此时线程是可以运行的,但是还没有真正的运行,只有等待CPU为其分配资源后线程才开始运行>
- 临时阻塞状态,当线程执行了sleep或者wait方法,就会进入临时阻塞状态。
- 死亡状态(结束状态):如果一个线程执行完了run()方法,就进入结束状态。
5. 线程安全问题
单一线程时,它只能在同一时间进行一项操作,所以永远不必担心有两个实体同时使用相同的资源。但是进入多线程环境后,它们就不再是孤立的,可能多个线程试图在同一时间访问同一个资源。比如两个线程同时从一个银行账户取款!
举一个多线程中常见的例子,三个售票窗口同时卖票,一共50张票,使用以下代码模拟:
class SaleTicket extends Thread {
// 设置为静态变量,因为是三个线程的贡献资源数据
static int num = 50;
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
while (true) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "售出了"
+ num + "号票");
num--;
} else {
System.out.println("售罄");
break;
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
SaleTicket a = new SaleTicket("A窗口");
SaleTicket b = new SaleTicket("B窗口");
SaleTicket c = new SaleTicket("C窗口");
a.start();
b.start();
c.start();
}
}
控制台输出如下:
A窗口售出了50号票
B窗口售出了50号票
B窗口售出了48号票
...
...
...
可以发现50号票被A、B窗口都卖出了,显然是错误的。这里就出现了线程安全问题,分析如下:
由控制台的输出我们可以做以下假设:
假设A窗口先抢到了CPU的资源,在第5行判断num>0,打印第6行的内容,此时CPU的资源被B窗口抢走,注意A窗口并没有执行num–,然后B窗口在第5行判断num>0,打印第6行的内容,执行num–,此时num等于49,然后CPU的资源被A窗口抢走,此时A窗口应该执行第6行代码,num–。现在num = 48,B窗口抢夺了CPU的资源,判断num>0,执行第6行,就会打印出”B窗口售出了48号票”。
在什么情况下会出现线程安全问题?
**
- 存在多线程
- 存在共享的资源(比如上例中的票)
- 存在多个任务来操作共享资源,当前任务进行到一半时,CPU的资源被别的线程抢夺
**
线程安全问题的解决办法:
同步机制:调用synchronized方法时对象会被锁定,不能被其他的线程访问,除非当前线程完成了被同步的任务,并解除锁定。
①:同步代码块
synchronized("锁对象"){
//需要被同步的代码
}
同步代码块要注意的事项:
- “锁对象”可以是任意的一个对象
- 多线程操作的锁对象必须是唯一的、共享的
- 在同步代码块中调用sleep方法,并不会释放锁
- 只有存在线程安全问题的时候才使用同步代码块,否则会降低线程执行的效率
使用同步代码块完成卖票,并解决线程安全问题:
class SaleTicket extends Thread {
// 设置为静态变量,因为是三个线程的贡献资源数据
static int num = 50;
static Object o = new Object();
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
while (true) {
// 同步代码块
synchronized (o) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "售出了"
+ num + "号票");
num--;
} else {
System.out.println("售罄");
break;
}
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
SaleTicket a = new SaleTicket("A窗口");
SaleTicket b = new SaleTicket("B窗口");
SaleTicket c = new SaleTicket("C窗口");
a.start();
b.start();
c.start();
}
}
②:同步函数
- 使用synchronized 修饰函数
- 如果是非静态的同步函数,锁对象是当前对象;如果是静态的同步函数,锁对象是当前函数所属的类的class对象。
考虑以下代码能不能解决线程安全的问题?
class BankThread extends Thread {
static int count = 5000;
public BankThread(String name) {
super(name);
}
@Override
public synchronized void run() {
while (true) {
if (count > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "取走了1000元,还剩余" + (count - 1000) + "元");
count = count - 1000;
} else {
System.out.println("账户余额不足...");
break;
}
}
}
}
public class Demo2 {
public static void main(String[] args) {
BankThread grandFather = new BankThread("爷爷");
BankThread grandMother = new BankThread("奶奶");
grandMother.start();
grandFather.start();
}
}
控制台输出如下:
爷爷取走了1000元,还剩余4000元
奶奶取走了1000元,还剩余4000元
奶奶取走了1000元,还剩余2000元
爷爷取走了1000元,还剩余1000元
奶奶取走了1000元,还剩余0元
账户余额不足...
爷爷取走了1000元,还剩余-1000元
账户余额不足...
因为synchronized 修饰的函数是非静态的,所以当线程grandMother执行取钱任务时,锁对象是grandMother;当grandFather 执行取钱任务时,锁对象是grandFather 对象。锁对象不是唯一的就不能解决线程安全问题
推荐使用同步代码块解决线程安全问题!!!
6. 死锁
有时两个或多个线程需要锁定几个共享对象。这时可能引起死锁(deadlock) ,也就是说,每个线程已经锁定一个对象,正在等待锁定另一个对象。考虑一种有两个线程和两个对象的情形。线程1已经锁定了 object1 ,线程2锁定了 object2 。现在线程1等待锁定 object2 ,线程2等待锁定object1 。每个线程都等待另一个线程释放自己需要的资源,结果导致两个线程都无法继续运行。
例如老公拿着银行卡,但是没有密码;老婆记着密码,但是没有银行卡,参考以下代码:
class DeadLock extends Thread {
public DeadLock(String name) {
super(name);
}
@Override
public void run() {
if ("老公".endsWith(Thread.currentThread().getName())) {
synchronized ("银行卡") {
System.out.println("老公拿到了银行卡,等待密码...");
synchronized ("密码") {
System.out.println("老公拿到了密码...");
System.out.println("老公可以去银行取钱了!!!");
}
}
} else if ("老婆".endsWith(Thread.currentThread().getName())) {
synchronized ("密码") {
System.out.println("老婆拿到了密码,等待银行卡...");
synchronized ("银行卡") {
System.out.println("老婆拿到了银行卡...");
System.out.println("老婆可以去银行取钱了!!!");
}
}
}
}
}
public class Demo3 {
public static void main(String[] args) {
DeadLock thread1 = new DeadLock("老公");
DeadLock thread2 = new DeadLock("老婆");
thread1.start();
thread2.start();
}
}
控制台输出如下:
老公拿到了银行卡,等待密码...
老婆拿到了密码,等待银行卡...
老公进入“银行卡”的同步代码块中,但是此时老婆进入了“密码”的同步代码块中。两个线程都会陷入无休止的相互等待状态。尽管这种情况并非经常出现,但一旦碰见,程序的调试就会变得异常艰难。就java语言本身来说,尚未提供防止死锁的措施,我们需要谨慎设计来避免。
但是上述例子中,只要“老公”线程跑的足够快,两者都是可以取到钱的!嘿嘿….
可以考虑将代码更改为如下,“老公”和“老婆”就有更大的可能性都能取到钱了!
else if ("老婆".endsWith(Thread.currentThread().getName())) {
try {
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized ("密码") {
System.out.println("老婆拿到了密码,等待银行卡...");
synchronized ("银行卡") {
System.out.println("老婆拿到了银行卡...");
System.out.println("老婆可以去银行取钱了!!!");
}
}
}
7. 线程间通信
wait():等待,线程执行了wait()方法,就会进入等待状态,等待状态的线程必须要被其他线程调用notify()方法才能唤醒。
notify():唤醒,唤醒等待的线程。
Note:
- wait()和notify()属于Object对象的方法
- wait()和notify()必须要在同步代码块或同步函数中才能使用
- wait()和notify()必须要由锁对象调用
当i为偶数时生产者生产苹果,i为奇数是生产者生产香蕉;生产者生产一个产品消费者消费一个产品。使用以下代码进行模拟:
//产品类
class Product {
boolean flag = false;// 产品是否已经存在
String name;
double price;
}
// 生产者
class Producer extends Thread {
Product p;
public Producer(Product p) {
this.p = p;
}
// 不断地生产
@Override
public void run() {
int i = 0;
while (true) {
synchronized (p) {
if (p.flag == false) {
if (i % 2 == 0) {
p.name = "苹果";
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.price = 5.0;
} else {
p.name = "香蕉";
p.price = 2.5;
}
System.out.println("生产者生产了:" + p.name + "价格是:" + p.price);
p.flag = true;
//生产完毕,唤醒消费者
p.notify();
i++;
} else {
try {
// 生产者已经生产完毕,等待消费者去消费
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
// 消费者
class Customer extends Thread {
Product p;
public Customer(Product p) {
this.p = p;
}
@Override
public void run() {
while (true) {
synchronized (p) {
if (p.flag == true) {
System.out.println("消费者消费了:" + p.name + "价格是:" + p.price);
p.flag = false;
//消费完了,唤醒生产者
p.notify();
}else{
try {
//产品没有被生产,等待生产者生产
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public class Demo4 {
public static void main(String[] args) {
Product p = new Product();// 产品
Producer producer = new Producer(p);// 生产者
Customer customer = new Customer(p);// 消费者
producer.start();
customer.start();
}
}
控制台输出:
生产者生产了:苹果价格是:5.0
消费者消费了:苹果价格是:5.0
生产者生产了:香蕉价格是:2.5
消费者消费了:香蕉价格是:2.5
...
...
说明:
1. 将p作为构造函数的参数传入,以实现p的共享;并且让p作为锁对象,调用wait和notify
2. 如果一个线程执行了wait()方法,那么该线程就会进入一个以”锁对象”为标识的线程池中并处于等待状态
3. 调用wait()方法会释放锁对象
4. 如果一个线程执行了notify()方法,那么就会唤醒上述线程池中的一个处于等待状态的线程
5. 调用notify()方法不能指定线程来唤醒,一般来说,先等待的线程先被唤醒
8. 停止线程
使用Thread类的stop()方法来停止一个线程,但此方法已经过时,不推荐使用。
如果需要停止一个处于等待状态的线程,可以通过布尔变量配合notify()或者interrupt()方法。
public class Demo5 extends Thread {
boolean falg = true;
public Demo5(String name) {
super(name);
}
@Override
public synchronized void run() {
int i = 0;
while (falg) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i++);
}
}
public static void main(String[] args) {
Demo5 d = new Demo5("线程A");
d.setPriority(10);
d.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
// 主线程i=80是,停止线程A
if (i == 80) {
d.falg = false;
/*同步代码块*/
// synchronized (d) {
// d.notify();
// }
/*使用interrupt清除等待状态,中断线程*/
d.interrupt();
}
}
}
}
线程A开启后,flag = true ,线程A会进入等待状态。然后执行主线程,当i=80,flag赋值为false,并且唤醒线程A;因为此时的flag为false,所以run方法里的代码会执行完毕,线程就被停止。
使用interrupt 强制清除处于等待状态的线程:如果线程在调用Object类的wait()、wait(long)或者wait(long,int)方法,或者Thread类的join()、join(long)、join(long,int)、sleep(long)或者sleep(long,int)方法后,执行interrupt会强制清除这些线程!并返回一个InterruptedException的异常。interrupt还可以指定清除哪个线程。
9. 守护线程和join()
守护线程的作用是在程序运行期间于后台提供一种“常规”服务,但是它并不属于程序的一个基本部分。一旦所有的非守护线程完成,程序就会终止,守护线程也会终止。可以调用isDaemon()查看一个线程是否为守护线程。线程默认不是守护线程,可以使用setDaemon(true)设置一个线程为守护线程。
以下代码为模拟软件更新包的后台下载,
public class Demo6 extends Thread {
public Demo6(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("更新包目前下载" + i + "%....");
if (i == 100) {
System.out.println("更新包下载完毕,准备安装...");
}
}
}
public static void main(String[] args) {
Demo6 d = new Demo6("后台线程");
//设置为守护线程
d.setDaemon(true);
System.out.println(d.isDaemon());
d.start();
for(int j = 0;j<100;j++){
System.out.println(Thread.currentThread().getName()+":"+j);
}
}
}
可以发现,当主线程停止时,后台下载更新的守护线程也会停止。
join():一个线程如果执行了join语句,那么就有新的线程加入,执行该语句的线程必须要让步给新加入的线程来完成任务,然后才能继续执行。
class Mom extends Thread{
@Override
public void run() {
System.out.println("妈妈开始做饭");
System.out.println("妈妈开始洗菜,切菜,炒菜...");
System.out.println("妈妈发现没有酱油了...让我去打酱油");
//我去打酱油
Me me = new Me();
me.start();
try {
/**
* mom执行了me.join()语句,me线程就加入到mom线程,并且mom线程会等待me线程执行完毕才会继续执行
* */
me.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("妈妈继续做饭");
System.out.println("全家人开心地吃晚饭...");
}
}
class Me extends Thread{
@Override
public void run() {
System.out.println("我一直往小卖铺走");
System.out.println("打完酱油...");
System.out.println("回家,并把酱油交给妈妈...");
}
}
public class Demo7 {
public static void main(String[] args) {
Mom mom = new Mom();
mom.start();
}
}
控制台输出如下:
妈妈开始做饭
妈妈开始洗菜,切菜,炒菜...
妈妈发现没有酱油了...让我去打酱油
我一直往小卖铺走
打完酱油...
回家,并把酱油交给妈妈...
妈妈继续做饭
全家人开心地吃晚饭...
最后,希望这篇线程相关的文章可以对java的初学者有所帮助,上文有不当的地方,请大家提出自己宝贵的意见!