Java线程大全

 

何为线程

可以把一个线程想象成流水线,多线程就是为了多个流水线同时工作(同时下载多个文件),也可以不把不同的事务分发到不同的流水线上一边下载(到缓存),一边保存(缓存保存到磁盘)

 

以下载文件为例,

单线程下载三个文件如下图,必须文件1下载完成后才能下载文件2



 

 

多线程下载三个文件情况如下图,三个文件同时开始下载。


 

多线程下载一个文件情况如下图,将下载一个文件分为下载线程和保存线程,两个线程以缓冲区作为中介,这是典型的生产者-消费者模式。


启动和结束线程

2.1 启动线程

Java中实现线程有两种方法

2.1.1 继承Thread,重写 void run() 方法

 

Thread thread = new Thread("ThreadName")
{    
        public void run()
        {
            // do something here
        }
};   
 thread.start();
 

    

 

上述代码首先创建了继承Thread,并覆盖run方法的匿名类。然后创建一名为"ThreadName"的线程,线程启动后会执行run()方法,run()返回后线程随即销毁。

Tip:给线程起个名字是很好的实践,这里给线程起名为"ThreadName",实际代码应该根据业务含义取名称,这对查看日志和调试都有很大的用处。

 

2.1.2 实现 Runnable 接口,放到Thread中执行

Runnable runnable = new Runnable()  
{
    public void run()  
    {
        // 线程中执行的代码
    }
};

Thread thread = new Thread(runnable, "ThreadName");
thread.start();  // 启动线程
 


上述代码首先创建了实现Runnable接口的匿名类,然后将匿名类的对象放入Thread中执行。

2.2 结束线程

run()方法结束后(return或抛出异常)线程随之结束。

线程的一些特性:

· 所有的Java代码都是在某个线程中执行的,所以在任一行Java代码中使用Thread.currentThread()都可以得到当前运行线程。 

· JVM允许多个线程并发执行,虽然同一时刻只能有一个线程占用CPU,但每个线程占有的时间片非常短,所以人类的感官上多个线程是并发执行的。 

· 当 JVM启动时,至少有一个用户线程运行,即执行某个类的main方法的线程。 

2.3 线程生命周期

可运行态(Runnable)  

start()被调用后线程进入可运行态。该状态不称为运行态是因为这时的线程并不总是一直占用处理机。特别是对于只有一个CPUPC而言任何时刻只能有一个处于可运行态的线程占用CPU

非运行态(Not Runnable) 

当以下事件发生时线程进入非运行态

A. suspend()方法被调用;

B. sleep()方法被调用

C. wait()方法被调用

D. 线程处于I/O等待。

死亡态(Dead)  

run()方法结束线程进入死亡态 。 

 


 

2.4 守护线程

有一种线程叫守护线程,和普通线程唯一的区别就是在线程启动前调用了线程对象的setDaemon(true)函数,如下面代码示例。

 

Thread thread = new Thread("ThreadName")  
{
    public void run()  
    {
        // 线程中执行的代码
    }
};
thread.setDaemon(true);
thread.start();  // 启动线程
 

 

如果在线程启动后调用setDaemon,会抛出IllegalThreadStateException 异常。

当程序中所有线程都是守护线程时,Java虚拟机就会退出程序。

线程同步 synchronized

在很多系统中都要用到递增的序列,下面是实现代码;我们期望nextSequence每次返回递增的数字。

public class SequenceMaker
{        
    public final static SequenceMaker instance = new SequenceMaker();
    
    private int sequence = 0;
    
    public int nextSequence()
    {
        if (sequence == Integer.MAX_VALUE )
        {
            sequence = 0;
        }
        sequence ++;       
        return sequence;
    }
}
 

然后我们编写测试代码,启动两个线程调用序列生成函数:

public class User extends Thread
{
    
    @Override
    public void run()
    {
        SequenceMaker sequenceMaker = SequenceMaker.instance;
        
        while(true)
        {
            int seq = sequenceMaker.nextSequence();        
            System.out.println(getName() + ": " + seq);
        }
    }
    
    public static void main(String[] args)
    {
        User user0 = new User();
        User user1 = new User();
        
        
        user0.setName("user0");
        user1.setName("user1");
        
        user0.start();
        user1.start();
        
    }
}
 

 

测试结果:发现有时候nextSequence返回了相同的数字。

user0: 2

user1: 2

user0: 4

user1: 5

user0: 6

问题在哪里呢?当两个线程按照下面的时序执行时并发生问题。

 



 

 

 

修改办法就是增加synchronized关键字,增加后代码如下:

   public synchronized int nextSequence()

{
     ...
     return sequence ;
}
 

增加synchronized关键字后nextSequence保证能够原子执行,如果两个线程同时调用一个函数,必须一个先执行,等执行完毕才能另一个线程才能执行该函数。


 

到这里我们的主角synchronized终于出现了。

synchronized更一般的形式是:

synchronized (令牌)// 请求令牌,如果不能获得,则阻塞直到获得令牌
{
    需要原子执行的代码
} // 释放令牌
 

多个线程执行上述代码时,线程首先要获得令牌,才能执行花括号之间的代码。

任何对象都可以是令牌,不同的令牌守护不同的代码,下面的代码每次都产生新的令牌,能起到同步作用。

public int nextSequence() // 错误的写法
{
    Object lock = new Object();
    synchronized (lock)
    {
        if (sequence == Integer.MAX_VALUE)
        {
            sequence = 0;
        }
        sequence++;
        return sequence;
    }
}
 

正确的写法如下,将令牌放到成员变量。

Object lock = new Object();
public int nextSequence()
{
    synchronized (lock)
    {
        if (sequence == Integer.MAX_VALUE)
        {
            sequence = 0;
        }
        sequence++;
        return sequence;
    }
}
 

如果SequenceMaker要提供两个独立的序列,或返回多个序列的方法,则代码如下

 

Object lock = new Object();
public int nextSequence()
{
    synchronized (lock)
    {
        if (sequence == Integer.MAX_VALUE)
        {
            sequence = 0;
        }
        sequence++;
        return sequence;
    }
}
 

nextSequence0nextSequence1可以并发执行(采用了两个令牌);

nextSequence0nextSequence0Bulk不能并发执行的(采用了同一个令牌);

 

 

函数声明的synchronized又是什么含义呢?

下面的代码中左边和右边是等效的。


生产者和消费者

生产者-消费者线程如下图,可以有生产者线程和消费者线程。


4.1 生产者-消费者实例

4.1.1 总体设计

总体类图如下:


 

ProductQueue类是最核心的类,设计成模板类。

 

4.1.2 向产品队列增加产品

首先看看向产品队列中增加产品的方法。

 

public void add(P p)
{
    synchronized (products)
    {
        // 1 如果队列满了,就等待消费者取走产品
        while (products.size() == maxProductCount)
        {
            try
            {
                products.wait(); // 线程被阻塞,阻塞期间锁被释放
            }
            catch (InterruptedException e)
            {
            }
        }
        
        // 2 将产品放入队列
        products.add(p);
        
        // 3 唤醒其它线程:等待的消费者去取
        products.notifyAll();
        
    }
}
 

这里出现了两个新的函数,令牌对象的 wait() 和 notifyAll();这两个方法来之Object类,JDK中这样描述:

 void

notifyAll() 
          Wakes up all threads that are waiting on this object's monitor.

 void

wait() 
          Causes current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object.

相关的函数还有:notifyAll()wait(long timeout)wait(long timeout, int nanos)

 void

notify() 
          Wakes up a single thread that is waiting on this object's monitor.

 void

wait(long timeout) 
          Causes current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed.

 void

wait(long timeout, int nanos) 
          Causes current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object, or some other thread interrupts the current thread, or a certain amount of real time has elapsed.

Tip:sleep()wait()的主要区别,

sleep() 使得线程在阻塞一段时间,不能得到CPU 时间,不会释放锁;

wait() 方法导致线程阻塞,并且该对象上的锁被释放;所以wait()方法必须在synchronized 范围内调用。

 

其中第一步判断队列是否已满,如果满了就等待消费者取走产品。注意这里用了一个while循环判断,而不是ifEffective Java中这样描述原因:

 

另一个线程可能得到了锁,并且在一个线程中调用notify的时刻,到等待线程醒过来的时刻之间,得到锁的线程已经改变了被保护的状态。

 

条件并没有成立,但是另一个线程可能意外地或恶意地调用了notify。在公有可访问的对象上等待,这些类实际上把自己暴露在危险的境地中。在一个公有可访问对象的同步方法中包含的wait都会出现这样的问题。

 

通知线程( notifying thread ) 在唤醒等待线程时可能会过度“大方”。例如,即使只有莫一些等待线程的条件已经满足,但是通知线程仍必须调用nofityAll

 

在没有通知的情况先等待线程也可能会醒来。这被称为“伪唤醒(spurious wakeup)”。虽然《The Java Language Specifition》并米有提到这种可能性,但是许多JVM实现都使用了具有伪唤醒功能的线程设施,尽管用得很少。

4.1.3 从产品队列取出产品

有了上面的知识,消费者线程从产品队列中获取产品的方法就容易实现了。

 

public P take()
{
    P p = null;
    synchronized (products)
    {
        // 1 如果队列为空,就等待生产者放入产品
        while (products.size() == 0)
        {
            try
            {
                products.wait(); // 线程被阻塞
            }
            catch (InterruptedException e)
            {
            }
        }
        
        // 2 将产品从队列中取走
        p = products.remove(0);
        
        // 3 唤醒其它线程:等待的生产者放入产品
        products.notifyAll();
        
    }
 

 

 

4.1.4 生产者和消费者

生产者代码:

 

 public void run()
{
    int count = 100;
    while (!productQueue.isDisposed())
    {
        productQueue.add(SequenceMaker.instance.nextSequence());
        count --;
        if (count == 0)
        {
            return;
        }
    }
}
 

 

消费者代码:

 

public void run()
{
    while (!productQueue.isDisposed())
    {
        Integer p = productQueue.take();
        System.out.println(getName() + " take: " + p);
    }
}
 

 

4.1.5 main函数代码

 

 

public static void main(String[] args) throws InterruptedException
    {
        PropertyConfigurator.configure("trace.log");
        
        ProductQueue<Integer> productQueue = new ProductQueue<Integer>(10);
        
        Producer producer0 = new Producer(productQueue);  
        Producer producer1 = new Producer(productQueue);
        producer0.setName("producer0");
        producer1.setName("producer1");
        
        Consumer consumer0 = new Consumer(productQueue); 
        Consumer consumer1 = new Consumer(productQueue); 
        consumer0.setName("consumer0");
        consumer1.setName("consumer1");
        
        producer0.start();
        producer1.start();
        
        consumer0.start();
        consumer1.start();
        
        // 等待生产者结束
        producer0.join();
        producer1.join();
        
        // 结束消费者
        productQueue.dispose();
	}
 

 

4.2 JDK本身对生产者-消费者的支持

接口:BlockingQueue,实现类:LinkedBlockingQueueArrayBlockingQueue等。

 

异步转为同步

Socket网络编程中,有发送线程和结束线程,查询数据为例,流程如下:


 

上图可以看出,查询数据的流程被分割到两个线程,代码的可读性、可维护性都有很差。

利用上面学习到的生产者和消费者的知识,我们可以把业务放到一个流程中。



 

Swing线程 

6.1 Swing线程基础

Swing最关键的线程就是事件派发线程(EDT):

将事件(键盘、鼠标)派发到各个组件,并负责调用绘制方法更新界面

按钮的事件响应方法actionPerformed并是在EDT中执行的。

如果在EDT线程中执行费时的操作,如查询数据库、写文件,就会导致界面卡住;如果不小心发生死锁还会导致界面一片空白。

Swing的规则是: 一旦Swing组件被显示,所有可能影响或依赖于组件状态的代码都应该在事件派发线程中执行。 

违反了上面的规则,如我们在其它线程中对界面做操作(更新按钮文字、增加树节点等)就会出现同步问题,典型的现象有:界面显示错乱。

 

 

6.2 invokeLaterinvokeAndWait

如何避免上面的异常呢,Swing提供了两个函数:

SwingUtilities.invokeLater(Runnable doRun)

SwingUtilities.invokeAndWait(Runnable doRun)

 

 

Runnable doRun = new Runnable() // 1 创建Runnable对象
{
    public void run()
    {
        frame.setTitle("xxxx");
    }
};

SwingUtilities.invokeLater(doRun); // 2 将Runnable对象放到EDT中执行
 

 

那么invokeLaterinvokeAndWait有和区别呢?

invoikeLaterinvokeAndWai都是把可运行对象排入事件派发线程的队列中

invokeLater在把可运行的对象放入队列后就返回

invokeAndWait一直等待知道已启动了可运行的run方法才返回

invokeLater的时序图,业务调用invokeLater后,继续执行代码:



invokeAndWait的时序图,业务调用invokeAndWait后,必须等run方法在EDT中执行后才能继续执行:

 


 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java线程池是一种用于管理和复用线程的机制。它可以在需要执行任务时,从线程池中获取可用的线程来执行任务,而不是每次都创建新的线程。这样可以减少线程创建和销毁的开销,提高系统的性能和资源利用率。 Java线程池通常由一个线程池管理器、工作队列和一组工作线程组成。线程池管理器负责创建、初始化和释放线程池,工作队列用于存储待执行的任务,工作线程则负责从队列中取出任务并执行。 使用线程池可以带来以下好处: 1. 提高系统性能:通过复用线程,降低了线程创建和销毁的开销,提高了系统的响应速度。 2. 控制资源消耗:通过限制线程的数量,可以控制系统同时运行的线程数量,避免资源过度消耗。 3. 提高系统稳定性:通过合理配置线程池参数,可以避免因为系统资源耗尽而导致系统崩溃。 在Java中,可以使用java.util.concurrent包中的Executor框架来创建和管理线程池。常用的线程池类型有FixedThreadPool、CachedThreadPool、SingleThreadExecutor等,每种类型适用于不同的场景和需求。 要使用线程池,通常需要以下步骤: 1. 创建线程池对象,并指定线程池的类型和参数。 2. 创建任务(Runnable或Callable对象)并提交给线程池。 3. 线程池会根据任务的类型和当前线程池状态,决定是创建新线程、复用现有线程,还是将任务加入到工作队列中。 4. 线程池中的工作线程会不断从队列中获取任务,并执行任务的run方法。 5. 当不再需要执行任务时,需要关闭线程池,释放资源。 通过合理使用线程池,可以有效管理线程,提高系统的性能和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值