多线程
学习线程先看三个概念:
- 程序:程序(program)是一个指令的集合。
进程:进程(process)正在进行中的程序,是一个静态的概念。
- 进程是程序的一次静态的执行过程,占用特定的地址空间。
- 每个进程都是独立的,有三部分组成,cup、data、code。
- 缺点:内存的浪费,cpu的负担。
线程:是进程中的一个”单一的连续控制流程”。
- 线程又被称为轻量级进程。
- 一个进程可以有多个并行的线程。
- 一个进程中的线程共享相同的内存单元——>可以访问相同的变量和对象,而且他们从 同一堆中分配对象——>通讯,数据交换,同步操作
> 线程的生命周期及五种基本形态:
先看一张关于线程生命周期的图(图是盗的,但想表达意思是真的【捂脸】)
线程的五种基本形态:
- 1、新生状态(对应上图的new):当线程被创建即进入新生状态,相当于Thread thread = new Thread();
- 2、就绪状态(对应Runable):当线程执行了.start()方法,即进入就绪状态,随时等待cpu的调度,这里要注意,进入就绪状态并不能理解为线程开始执行。 (thread.start();)
- 3、运行状态(对应Running):当cpu对已经进入就绪状态的线程进行调度的时候,线程才算开始运行。也可以说是线程要想运行,必须是出于就绪状态,并且等待cup对其进行调度。
4、阻塞状态(对应Blocked):正在运行的线程,由于某种特定的原因,cpu暂时放弃对它的调度,停止执行,即进入阻塞状态,直到此线程进入就绪状态才有机会再次运行。根据其阻塞的原因,可分为三种:
- a、等待阻塞:线程运行时调用了wait()方法,进入等待阻塞状态。可通过notify()或者notifyAll()唤醒。
- b、同步阻塞:线程在获取同步锁synchronized失败,(可能被其他线程占用)进入到同步阻塞状态。
- c、其他阻塞:调用线程中的sleep()方法,或者join()方法,线程进入阻塞状态。当sleep时间到、join ()等待时间终止或者I/O处理完毕,进入就绪状态。
5、死亡状态:线程线程执行完run()方法或者因异常退出而终止,该线程就结束了生命周期。
再来个简单的结构图:
了解了线程的基本形态,那么如何创建多线程呢?
> 多线程的创建
在java中,多线程的创建有三种基本的形式,由于知识有限【尴尬】,在这里只给大家介绍常用的两种基本形式:
- 1、继承Thread类。
- 2、实现Runable接口
第一种:继承Thread类,重写run()方法:
代码实例:
public class ThreadWork1 extends Thread { //继承Thread类
private int num = 10;
@Override
public void run() { //重写run()方法
// TODO Auto-generated method stub
super.run();
for (int i = 0; i < 100; i++) { //用5个线程输出10以内的偶数
if(num > 0){
num--;
if( num % 2 == 0) {
System.out.println(Thread.currentThread().getName() + "输出" + num);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public class ThreadTest1 {
//测试类,定义5个线程,并且调用.start()方法启动线程
public static void main(String[] args) {
Thread thread1 = new ThreadWork1();
Thread thread2 = new ThreadWork1();
Thread thread3 = new ThreadWork1();
Thread thread4 = new ThreadWork1();
Thread thread5 = new ThreadWork1();
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
输出结果:
第二种:实现Runable接口,重写run()方法。
代码实例:
public class ThreadDemo implements Runnable{ //实现Runable接口
private int ticket = 5;
@Override
public void run() { //重写run方法,用四个线程,模拟卖票
// TODO Auto-generated method stub
for(int i=0;i<100;i++){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"我正在出售第"+(ticket--)+"张票");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
public class ThreadTest { //测试类,定义四个线程并给线程起名字为"窗口X",并启动线程
public static void main(String[] args) {
ThreadDemo4 t4 = new ThreadDemo4();
Thread thread1 = new Thread(t4,"窗口一:");
Thread thread2 = new Thread(t4,"窗口二: ");
Thread thread3 = new Thread(t4,"窗口三: ");
Thread thread4 = new Thread(t4,"窗口四 :");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
输出结果:
根据输出的结果发现,窗口一和窗口三都在出售第四张票,按照实际情况来说,是不会出现这样的情况的(就比如坐火车,买了同样的一张票该怎么办,到时候就必定引起矛盾了,然后。。。扯远了)所以我们就引入了线程的同步问题。线程同步问题等下再说,先看这两种创建线程的形式,继承和实现接口有什么区别呢?
不同的地方在于创建线程时的不同:
继承---> Thread thread1 = new ThreadWork1();
实现接口---> ThreadDemo4 t4 = new ThreadDemo4();
Thread thread1 = new Thread(t4);
这两种方法都可以成功创建线程,具体需要用什么形呢?如果一个类已经继承了别的父类,那就得需要用到实现Runable接口来创建线程了。
线程的安全性问题
我们刚才说到模拟卖票的实例,有两个窗口同时卖了同一张票,甚至多次运行还会出现0和负数,那么遇到这种问题该如何解决呢?
这就需要引入多线程的同步(synchronized)
public void run() {
while(true){
synchronized (this) {//通常将当前对象作为同步对象
if (tick>0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"卖票:"+tick--);
}
}
}
}
在run()方法中使用了synchronized(this){}同步代码块,一般来说,同步代码块会放在需要同步数据的位置,也可以放在方法中,让方法实现同步。
public void run() {
while(true){
sale();
}
}
public synchronized void sale(){ //同步放在方法中
if (tick>0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"卖票:"+tick--);
}
}
同步监视器:
* synchronized(obj){}中的obj称为同步监视器
* 同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器
* 同步方法中无需指定同步监视器,因为同步方法的监视器是this,也就是该对象本身
同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器未锁,锁定并访问
简单的来说,同步就是让一个线程操作共享的数据,其他线程等待,该线程执行完另一个线程开始执行。
同步的前提:
- 必须有两个或者两个以上的线程。
- 必须是多个线程使用同一个资源。
- 必须保证同步中只能有一个线程在运行。
同步可以保证资源共享操作的正确性,但过多的同步会导致死锁问题。
死锁一般是线程在互相等待,都没有执行。解决方法,引入了java多线程的通讯。
线程的通讯
需要用到消费者和生产者问题:
/**
* 实体类
* @author lt
*
*/
public class Q {
private String name;
private int num;
boolean value = false;
/**
* 消费
* @return
*/
public synchronized String get() {
if(!value){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("消费第"+num+"瓶"+name);
System.out.println("------------");
value = false;
notify();
return num+name;
}
/**
* 生产
* @param name
* @param num
*/
public synchronized void set(String name,int num) {
if(value){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.name = name;
this.num = num;
value = true;
System.out.println("生产第"+num+"瓶"+name);
notify();
}
}
.定义实体类,定义属性及消费方法,生产方法。
/**
* 生产者类
* @author lt
*
*/
public class Producer implements Runnable{
Q q = new Q();
Thread thread;
public Producer(Q q) {
// TODO Auto-generated constructor stub
this.q = q;
thread = new Thread(this,"生产");
thread.start();
}
@Override
public void run() {
// TODO Auto-generated method stub
int num = 1;
while(true){
q.set("娃哈哈", num++);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
.定义生产者类,实现接口,重写方法。
/**
* 消费者类
* @author lt
*
*/
public class Consumer implements Runnable{
Q q = new Q();
Thread thread;
public Consumer(Q q) {
// TODO Auto-generated constructor stub
this.q = q;
thread = new Thread(this,"消费");
thread.start();
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
q.get();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
.定义消费者类,同样实现接口,重写方法。
public class AppMain {
//测试类
public static void main(String[] args) {
// TODO Auto-generated method stub
Q q = new Q();
new Producer(q);
new Consumer(q);
}
}
运行结果:
由结果发现,生产一瓶,消费一瓶,这样保证了资源的正确性,也不会存在线程死锁问题。在实体类中,定义了一个boolean类型的变量模拟死锁问题,运用wait()方法和notify()方法,让线程停止然后另一个线程执行完毕后再唤醒,有效的解决的因资源同步出现的死锁问题。
——————————————————————————————————————————
本人知识有限,如有错误或者不准确的地方,感谢指正【抱拳】。