欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

疯子在思考之java 线程的那点事儿

程序员文章站 2022-03-05 10:46:41
...
很长时间没写博客了,最近事情比较多
之前在文章中提到过tomcat 的main函数在哪?被很多朋友拍砖了
今天继续就这话题展开,先了解几个线程有关的概念
1、多线程 multithread
为什么要用多线程?就是让cpu别太闲,有空就要干活,提高效率。
2、线程池 threadpool
为什么要用线程池,所有跟池相关的,如connectionPool(数据库连接池),ajax request请求对 象池、线程池等都是为了减少对象new所带来的开销.
3、线程安全 thread safe
所谓的线程安全就是指多线程的运行结果与单线程的运行结果一致,java 通过synchronized和threadlocal等解决线程安全问题。
线程不安全是由于对共享资源(如static 变量 单例成员变量 文件 数据库 缓存等)的同步写操作而引起的。

tomcat 的main在这里
它会启动一个serviceSoket监听客户端socket请求.
org.apache.catalina.startup.Bootstrap.java
具体功能有一本书《深入剖析Tomcat》有兴趣的朋友可以了解一下。
我们先用最简单的socket 看看他的工作原理
服务器 socket代码
package service;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class ServiceSocketDemo {

	/**
	 * @author zlz
	 * 
	 * @time 2013-7-26下午5:00:04
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
                //服务器端启动后会不停地监听该端口请求
		ServerSocket serverSocket = new ServerSocket(1999);
		while (true) {
                        //接受请求
			Socket socket = serverSocket.accept();
			BufferedReader br = new BufferedReader(new InputStreamReader(
					socket.getInputStream()));
			String msg = null;
			if ((msg = br.readLine()) != null) {
				System.out.println("服务器端收到—->" + msg);
			}
			PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
                         
//处理后并输出到客户端
			pw.println("hi");
			socket.close();
		}
	}
}



客户端socket代码
package client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;

public class ClientSocketDemo {

	/**
	 * @author zlz
	 * 
	 * @time 2013-7-26下午5:03:52
	 * @param args
	 * @throws IOException
	 * @throws UnknownHostException
	 */
	public static void main(String[] args) throws UnknownHostException,
			IOException {
                //请求127.0.0.1:1999端口
		Socket socket = new Socket("127.0.0.1", 1999);
		PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
                //发送hello 并等待服务器端处理
		pw.println("helllo");                
		BufferedReader br = new BufferedReader(new InputStreamReader(
				socket.getInputStream()));
		String msg = null;
		if ((msg = br.readLine()) != null) {
			System.out.println("客户端收到—->" + msg);
		}
	}

}


沿着多线程的思路给这个最简单的socket加上多线程
方案1:将请求的数据扔给线程(这个方案是行不通的,因为数据处理完毕之后无法返回给客户端,需要返回到主线程处理,这就相当于是脱了裤子放屁,多此一举)
方案2:将socket对象扔给线程(这一点其实很重要,主线程不要再关心子线程的处理结果,下面要继续说到主线程需要关心子线程结果的情况 mapreduce)

方案2是多线程的一种实现方案,但是还不够优化,每次请求都要new一个线程,增大了服务器的压力,所以tomcat引入了threadpool的概念。
实现threadpool与其他的pool不太一样,因为线程程行完成之后就不能再restart了,即一旦执行完成将不能再复用。那么怎么实现线程池呢?

解决办法:在线程内while无线循环,永远不死。

继续拿tomcat的例子
tomcat会有一个sockt队列(当然对它的访问要线程安全的)
主线程负责生产
子线程负责处理
即生产者/消费者模式
显然这里比上边的多线程多了一个队列,这是我所理解的线程池概念,有异意大家批评。

刚才提到这里的主线程不需要子线程返回结果
而有些场景我们是需要子线程反馈的,比如mapreduce,它是将一份数据(一般比较大)
分成N份给N个线程这是map的过程,然后每个线程的处理结果在汇总到主线程这是reduce的过程。那么就需要线程间通信。
线程通过需要全局变量,即主线程和子线程共享能够访问的变量。
举一个简单例子
比如一本汉语字典,要查一下字典中一共有多少个字,那么我们分成N个线程去同步处理。
需要一个变量去统计已经处理的线程总数(dealedThreadCount),还有一个字典对象。
这个变量一定是共享的(或者是static 或者所在类是单例的)
那么主线程的处理逻辑应该是这样的
伪代码:
static Integer dealedThreadCount
synchronized(dealedThreadCount){
    for(int i=0;i<n;i++{
      //子线程处理
      new DealThread(i).start();
    }
    //主线程等待
    dealedThreadCount.wait();
}

子线程代码
...
dealedThreadCount++;
if(dealedThreadCount==n)
{
dealedThreadCount.nodify();通知主线程reduce
}

总结:
synchronized的变量只要是全局共享的即可,可以没有任何意义,为了实现线程间通信
需要synchronized wait notify 辅助完成
一般出现wait notify的情况一定会出现synchronized,反之则不一定。



最后一点关于线程安全的threadlocal
见这里
http://lizhizhang.iteye.com/admin/blogs/1909765

这里继续说一下,上边的链接讲了threadlocal原理及java sdk 的代码分析。
那么什么场景应用呢?
我所了解的有两个地方
第一 数据库访问时的session对象
第二 不知道朋友们思考过没有,strtus 2.0有一个actionsupport对象,这里会注入request response等对象
是因为他们是多实例对象所以不会有问题,那么如果我想把它定义成单例的(spring mvc是这样做的)就会有线程安全问题。而且我们同样要继承该类,那么就需要threadlocal变量。


线程安全问题非常值得注意
比如jdk里的很多对象都是线程不安全的,而有些变量看起来是不安全的,而实际上又是安全的。举两个例子

//线程安全的 getInstance一般为单例的方法,但jdk里每次都会new 一个变量出现。
Calendar c=Calendar.getInstance();
 /**
     * Gets a calendar using the default time zone and locale. The
     * <code>Calendar</code> returned is based on the current time
     * in the default time zone with the default locale.
     *
     * @return a Calendar.
     */
    public static Calendar getInstance()
    {
        Calendar cal = createCalendar(TimeZone.getDefaultRef(), Locale.getDefault());
	cal.sharedZone = true;
	return cal;
    }

 private static Calendar createCalendar(TimeZone zone,
					   Locale aLocale)
    {
	// If the specified locale is a Thai locale, returns a BuddhistCalendar
	// instance.
	if ("th".equals(aLocale.getLanguage())
	    && ("TH".equals(aLocale.getCountry()))) {
	    return new sun.util.BuddhistCalendar(zone, aLocale);
	} else if ("JP".equals(aLocale.getVariant())
		   && "JP".equals(aLocale.getCountry())
		   && "ja".equals(aLocale.getLanguage())) {
	    return new JapaneseImperialCalendar(zone, aLocale);
	}	    

	// else create the default calendar
        return new GregorianCalendar(zone, aLocale);	
    }


第二个
DateFormat s=SimpleDateFormat.getInstance();
都是非单例对象,即是线程安全的
但是每次new的开销是比较大的,当数据量访问量非常大时会导致cpu过高,解决办法也是通过threadlocal


jdk中其他线程问题,各位大拿可以补充,谢谢!