一、进程与线程的概念
进程是内存中运行的一个应用程序,线程是进程中的一个执行单元。
一个程序可以有多个进程,一个进程可以有多个线程且至少有一个线程。
二、Java中创建线程的两种方式
- 定义Thread的子类,并重写该类的run方法
public class MyThread extends Thread{
@Override
public void run() {
for (int i=0;i<20;i++){
System.out.println("齐天大圣:"+i);
}
}
}
public class HelloThread {
public static void main(String[] args) {
// 创建子类实例,即创建了线程对象
MyThread myThread = new MyThread();
myThread.start();// 调用start()方法启动该线程
}
}
- 定义Runnable接口的实现类,并重写该接口run()方法
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<10;i++){
// 输出内容:线程名称:i
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class HelloThread {
public static void main(String[] args) {
// 创建该接口实现类的实例
MyRunnable mr = new MyRunnable();
// 以此实例mr 作为Thread的target来创建Thread类的对象,
// 注意!该对象tr才是真正的线程对象(其实这种创建方式也就实现了同一线程的资源共享!)
Thread tr= new Thread(mr,"线程1");
tr.start();
}
}
三、两种创建线程的方式Thread和Runnable的区别
若一个类继承Thread,则不适合资源共享。若实现了Runnable,则很容易资源共享。(资源共享简单举例就是多个窗口卖100张票那个例子)
总结: 实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
(其实,不管是继承Thread类还是实现Runnable接口来创建线程,最终都是通过Thread的对象API来控制线程的。)
四、线程安全
这里举最简单的卖票案例来说明
首先定义一个实现Runnable接口的类Ticket
public class Ticket implements Runnable{
private int tickets=100;
@Override
public void run() {
while (true){
if (tickets>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 正在卖第:"+tickets--+"张票");
}
}
}
}
创建测试类,开始卖票
public class TestDemo {
public static void main(String[] args) {
// 创建线程任务对象
Ticket ticket = new Ticket();
// 创建3个窗口对象(Runnable的好处在此就体现出来了,3个线程可以同时共享这100张票,实现了资源共享)
Thread t1 = new Thread(ticket,"窗口1");
Thread t2 = new Thread(ticket,"窗口2");
Thread t3 = new Thread(ticket,"窗口3");
// 3个窗口开始卖票
t1.start();
t2.start();
t3.start();
}
}
输入结果:
此时出现了线程安全的问题,5出现2次,且票数出现了0和-1
这是因为当线程1在执行时,还没执行完,线程2或者3也执行开始,导致票数在不同的线程里出现了重复或者不存在。
解决方案:
- 利用java中提供的同步机制synchronized关键字来解决
public class Ticket implements Runnable{
private int tickets=100;
private Object lock = new Object();
@Override
public void run() {
while (true){
synchronized (lock){ // lock 是一个锁对象,该对象可以是任意类型,但是多个对象要使用同一把锁才能起到效果
if (tickets>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 正在卖第:"+tickets--+"张票");
}
}
}
}
}
2.Lock锁:java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
public class Ticket implements Runnable{
private int tickets=100;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock(); // 加同步锁
if (tickets>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 正在卖第:"+tickets--+"张票");
}
lock.unlock(); // 释放同步锁
}
}
}
五、 线程状态
线程状态我们不需要非得去理解原理,只需简单知道线程在创建后会有这么几种状态即可。
Timed Waiting(计时等待),最常见的就是Thread.sleep(),sleep时间到期后线程会自然苏醒进入Runnable(可运行)状态。
Blocked(锁阻塞):和上面介绍的同步机制类似,线程1和线程2同时运行并使用了一把锁,当A拿到锁的时候B此时就进入Blocked状态,A释放锁后,B拿到锁B再进入Runnable状态。
Waiting(无限等待):简单来说就是一个线程A在无限期的等待另一个线程B执行唤醒线程A这一动作的状态。可通过下面代码进一步来理解此状态:
public class TestDemo2 {
// 定义锁对象
public static Object obj = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while(true){
synchronized (obj){
try{
System.out.println(Thread.currentThread().getName()+"获取到锁对象,调用wait方法,进入waiting状态,释放锁对象");
obj.wait();
//obj.wait(5000); 等待5秒,5秒内若被唤醒就解除waiting状态,没有到5秒后自动解除waiting状态
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"从waiting状态醒来,且获取到了锁对象,继续执行了");
}
}
}
},"线程A").start();
new Thread(new Runnable() {
@Override
public void run() {
while(true){
try{
System.out.println(Thread.currentThread().getName()+"等待3秒钟");
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (obj){
System.out.println(Thread.currentThread().getName()+"获取到锁对象,调用notify方法,释放锁对象");
obj.notify();
}
}
}
},"线程B").start();
}
}
当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入 了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了 notify()方法,那么就会将无限等待的A线程唤醒。
注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入 Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。
下图所示为几种线程状态的运行图解:
六、线程池
为什么要有线程池这个概念呢?这是因为当服务器中并发的线程数量很多时,如果用传统的创建线程方法频繁的创建线程,会导致系统的效率大大降低,因为一个线程从创建到销毁都是需要时间的。
由此引入线程池,线程池就是一个可以容纳多个线程的容器,其中的线程可以反复使用,避免了频繁创建线程销毁线程的这些动作。
线程池的工作原理,如下图所示:
合理运用线程池的好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内 存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
七、线程池的使用
Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程 池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService 。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。