Java基础总结八之多线程(一)

1:多线程

(1)多线程:一个应用程序有多条执行路径;

进程:正在执行的应用程序;

线程:进程的执行单元,执行路径;

单线程:一个应用程序只有一条执行路径;

多线程:一个应用程序有多条执行路径;

多进程的意义:提高CPU的使用率;

多线程的意义:提高应用程序的使用率。

(2)Java程序的运行原理及JVM的启动时多线程的吗?

A:Java命令去启动JVM,JVM会启动一个进程,该进程会启动一个主线程。

B:JVM的启动是多线程的,因为它最少有两个线程启动了,主线程和垃圾回收线程。

(3)多线程的实现方案:

A:继承Thread类:

B:实现Runnable接口:

(4)一个简单的多线程实例(继承Thread类):


public class ThreadDemo extends Thread {

	public ThreadDemo() {
	}

	public ThreadDemo(String name) {
		super(name);
	}
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(getName() + ":" + i);
		}
	}

	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo("窗口一");
		ThreadDemo td2 = new ThreadDemo("窗口二");
		td.start();
		td2.start();
	}
}
//执行结果:(部分)
窗口一:0
窗口一:1
窗口二:0
窗口一:2
窗口二:1
窗口一:3
窗口一:4
窗口一:5

上诉演示的是带参构造函数实例化,无参函数实例化如下:


public class ThreadDemo extends Thread {
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 100; i++) {
			System.out.println(getName() + ":" + i);
		}
	}

	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		ThreadDemo td2 = new ThreadDemo();
		td.start();
		td2.start();
	}
}
//执行结果:(部分)
Thread-0:0
Thread-1:0
Thread-0:1
Thread-1:1
Thread-0:2
Thread-1:2
Thread-0:3

实际上,可以发现,当使用的是无参函数实例化时,通过getName()函数,可以自动生成对应的线程名,由底层源码可以知道:

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

既然有了getName(),当然其对应的也存在setName(),底层使用name[]字符数组对线程的名字进行存储,因此无参的构造函数同样也可以給他起一个自己想要的名字:

td.setName("线程一");
td.setName("线程二");

那么我们可以知道的是Thread类的子类可以通过父类中getName()方法,得到自己本线程的名字,那么执行main方法的线程名字是什么呢?main方法是程序的入口,由JVM调用:Thread类提供了这样一个静态方法,来返回对当前正在执行的线程对象的引用。

public class ThreadDemo {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName());
	}
}
//执行结果:
main

(5)线程的调度:

假设CPU是一个的话,同一个时间CPU只能执行一条指令,线程只有得到CPU的使用权才能执行指令,那么线程是如何被调用的呢?Java又是采用的那种线程调度机制呢?

线程的调度模型有两种:分时调度模型和抢占式调度模型。

分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。

抢占式调度模型:优先让优先级高的线程使用CPU。如果线程的优先级相同,那么会随机选择一个。优先级高的线程获取的CPU时间相对多一些。

Java采用的是抢占式调度模型。

(6)线程优先级:(线程的默认优先级为5)

获取线程的优先级:public final int getPriority()

更改线程的优先级:public final void setPriority(int newPriority)

最大优先级:public static final int MAX_PRIORITY  :10

最小优先级:public static final int MIN_PRIORITY   :1

默认优先级:public static final int NORM_PRIORITY:5

但需要注意的是:某个线程设置了优先级为最高,并不能保证说一定是他执行结束其他线程才能得到CPU,而是保证的是其获得CPU的可能性是最高的。

(7)线程控制:

A:休眠线程:public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 100; i++) {
			System.out.println(getName() + ":" + i);
			try {
				Thread.sleep(1000);//休眠时间设置为1s
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
//执行结果:
当该线程执行到这的时候就会休眠一秒

B:加入线程:public final void join():等待这个线程死亡。其他的线程才能开始得到CPU。

public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo("窗口一");
		ThreadDemo td2 = new ThreadDemo("窗口二");

		td.start();
		try {
			td.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		td2.start();
	}
//执行结果:
窗口一的线程任务执行结束后才执行窗口二的

C:礼让线程:public static void yield():暂停当前正在执行的线程对象,并执行其他线程。


	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 100; i++) {
			System.out.println(getName() + ":" + i);
			Thread.yield();
		}
	}

礼让线程只能在一定程度上保证两个线程的有序性(多个线程的和谐性)。

D:守护线程:public final void setDaemon(boolean on):当运行的唯一线程都是守护进程线程时,Java虚拟机将退出。​​​​on - 如果 true ,将此线程标记为守护线程。当主线程结束后守护线程会随着主线程的结束而立即终止。但需要注意的是:在设置某个线程为守护线程的时候,要在启动前设置其为守护线程。

        public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo("窗口一");
		ThreadDemo td2 = new ThreadDemo("窗口二");
		td.setDaemon(true);
		td.setDaemon(true);
		td.start();
		td2.start();
		Thread.currentThread().setName("主窗口");
		for (int i = 0; i < 5; i++) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}

E:中断线程:

public final void stop():让线程停止,过时了。

public void interrupt():中断线程,把线程的状态终止,并抛出一个InterruptedException。

import java.util.Date;

public class ThreadDemo extends Thread {
	public ThreadDemo(String name) {
		super(name);
	}

	@Override
	public void run() {
		System.out.println("当前时间是:" + new Date());
		try {
			Thread.sleep(10000);
		} catch (InterruptedException e) {
			System.out.println("线程被中断了");
		}
		System.out.println("当前时间是:" + new Date());
	}

	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo("窗口一");
		td.start();
		try {
			Thread.sleep(3000);
			td.interrupt();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

(8)线程的生命周期:

A:新建:创建线程对象

B:就绪:有执行资格但没有执行权

C:运行:有执行资格有执行权

阻塞:由于一些操作让线程处于这种状态,没有执行资格,没有执行权,但可以通过其他方式使其变为就绪状态。

D:死亡:线程对象变成垃圾等待回收

(9)一个简单的多线程实例(实现Runnable接口):

public class RunnableDemo implements Runnable {

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}

	public static void main(String[] args) {
		RunnableDemo rd = new RunnableDemo();
		Thread t1 = new Thread(rd,"窗口一");
		Thread t2 = new Thread(rd,"窗口二");
		t1.start();
		t2.start();

	}
}
//注意这里Thread构造方法传进去的是Runnable接口作为参数

(10)讲了Runnable接口实现多线程后,就先作一个小结:总结一下继承Thread类和实现Runnable接口创建线程对象的步骤:

方式一:继承Thread类:

A:自定义类继承Thread类

B:在自定义类中重写run()方法

C:创建自定义类对象

D:启动线程对象

方式二:实现Runnable接口:

A:自定义类实现Runnable接口

B:重写run()方法

C:创建自定义类对象

D:创建Thread对象,并把C步骤的对象作为构造参数传递

有了方式一创建线程为什么还会有方式二呢?

1、可以避免由于Java单继承带来的局限性(举例:一个类已经继承了起父类,但又想实现多线程......)

2、适合多个相同程序的代码去处理同一个资源的情况,把线程相同程序的代码、数据进行有效的分离,较好的体现了面向对象的设计思想。(举例,当自定义类继承的是Thread类时,若自定义类中有成员变量,那么每实例化一次,该成员变量就多做一份拷贝,而如果实现的是Runnable接口,没创建一个对象,其传入的参数都是该实现Runnable接口的实现类的一个实例,副本当然也就一个,相当于对同一资源进行操作)。

(11)run()方法和start()方法有什么区别?

run()方法直接调用仅仅是普通方法;start()方法先是启动线程再由JVM调用run()方法。

(12)判断一个程序是否会出现线程安全问题:

A:看是否是多线程环境

B:看是否有共享数据

C:看是否有多条语句操作共享数据

满足以上任意一点都有可能出现多线程环境。那么如何解决线程安全问题呢?可以知道的是对于A,B两点,如果满足的话那么我们一般是无法改变的,我们只好想办法改变C。思想是:把多条语句操作共享数据的代码包成一个整体,让某一线程来执行的时候别人不能来执行。Java也提供了同步机制:

同步代码块:synchronized(对象){需要同步的代码块}

//同步机制实现简单的抢票功能
public class SellTicketRunnableDemo implements Runnable {

	private int ticket = 100;
	// 创建锁对象
	private Object object = new Object();

	@Override
	public void run() {
		while (true) {
			synchronized (object) {
				if (ticket > 0) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "正在出售第:" + ticket-- + "张票");
				}
			}
		}
	}

	public static void main(String[] args) {
		SellTicketRunnableDemo strd = new SellTicketRunnableDemo();
		Thread t1 = new Thread(strd, "窗口一");
		Thread t2 = new Thread(strd, "窗口二");
		Thread t3 = new Thread(strd, "窗口三");
		t1.start();
		t2.start();
		t3.start();
	}
}
//注意的是:
给操作同一共享数据的所有操作代码上锁,该锁必须是同一把锁

同步的特点:前提是多线程。多线程使用的是同一个锁对象。

同步的好处:同步的出现解决了多线程的安全问题。

同步的弊端:当线程相当多时,因为每个线程都回去p-anduan同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

(13)对于(12)点中的代码也能写成如下方式:


public class SellTicketRunnableDemo implements Runnable {

	private int ticket = 100;
	// 创建锁对象
	private Object object = new Object();

	@Override
	public void run() {
		while (true) {
			SellTicket();
		}
	}

	private void SellTicket() {
		synchronized (object) {
			if (ticket > 0) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "正在出售第:" + ticket-- + "张票");
			}
		}
	}

	public static void main(String[] args) {
		SellTicketRunnableDemo strd = new SellTicketRunnableDemo();
		Thread t1 = new Thread(strd, "窗口一");
		Thread t2 = new Thread(strd, "窗口二");
		Thread t3 = new Thread(strd, "窗口三");
		t1.start();
		t2.start();
		t3.start();
	}
}

哎呦,这个时候有人就想说,你把这个同步代码块提取到一个方法里面当然没有问题了, 线程一进入到方法就发现synchronized关键字,就知道同步了。的确,上诉的写法是没有任何问题的,该写法只是用来从同步代码块过渡到同步方法上去的。同步代码块的锁对象可以是任意对象,但是同步方法的锁对象又是什么呢???this。那么静态同步方法的锁对象又是谁呢?类的字节码文件对象(反射)。

(14)总结一下之前Java基础中讲到的线程安全的类:

A:Vector    B:StringBuffer   C:Hashtable

通过查看这些类的底层实现,可以知道这些类所有可调用的方法都加了synchronized关键字,保证了线程的安全。

当然集合工具类Collections也提供了方法供其他非线程安全的集合转化为线程安全的集合:

List<String>list=Collections.synchronizedList(new ArrayList<>());
//将线程不安全的ArrayList集合转化为线程安全的集合。

注:以上文章仅是个人学习过程总结,若有不当之处,望不吝赐教。

猜你喜欢

转载自blog.csdn.net/m0_37265215/article/details/81588549