Task多线程问题
问题原因
一个Executor是一个进程,一个进程中可以同时运行多个线程Task,如果多个Task使用了共享的成员变量,就会出现线程不安全的问题
案例
需求
使用spark将日期字符串转换成long类型时间戳
样例数据
2019-11-06 15:59:50
2019-11-06 15:59:51
2019-11-06 15:59:52
2019-11-06 15:59:53
2019-11-06 15:59:54
2019-11-06 15:59:55
2019-11-06 15:59:56
2019-11-06 15:59:57
2019-11-06 15:59:58
错误示范:
将new SimpleDateFormat定义为成员对象,并使用object修饰的工具类,就会存在线程安全问题
package com.doit.spark.day08
import java.text.SimpleDateFormat
object DateUtils2 {
//将new SimpleDateFormat定义为成员对象,并使用object修饰的工具类,就会存在线程安全问题
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
def dataToLong (dateU: String) ={
val time: Long = sdf.parse(dateU).getTime
time
}
}
package com.doit.spark.day08
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object DateDemo2 {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("DateDemo")
val setMaster = args(0).toBoolean
if (setMaster){
conf.setMaster("local[*]")
}
val sc = new SparkContext(conf)
val textFile = args(1)
val line: RDD[String] = sc.textFile(textFile)
val maped: RDD[Long] = line.map(x => {
val timeLong: Long = DateUtils2.dataToLong(x)
timeLong
})
println(maped.collect().toBuffer)
}
}
报如下错误(几率性的),因为多个Task去抢一个Executor中的SimpleDateFormat对象,该对象需要对时间进行格式转换,可能一个Task转换到一半的时候,另一个Task把进程抢了过去,就会报NumberFormatException的错误
解决方式一:
将new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”)写在局部变量的位置上,但是这样每来一条数据就会在该方法中new一个对象,浪费大量的内存空间,不推荐
package com.doit.spark.day08
import java.text.SimpleDateFormat
object DateUtils2 {
def dataToLong (dateU: String) ={
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val time: Long = sdf.parse(dateU).getTime
time
}
}
解决方式二:
给dataToLong方法加锁synchronized,但这样会使程序的执行速度变慢,因为这样相当于把多线程变成了单线程
package com.doit.spark.day08
import java.text.SimpleDateFormat
object DateUtils2 {
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
//给方法加锁
def dataToLong (dateU: String) = synchronized{
val time: Long = sdf.parse(dateU).getTime
time
}
}
解决方式三:
不使用SimpleDateFormat转换时间格式,因为它没有线程锁,可以使用另一个线程安全的工具类FastDateFormat,它里面有线程锁,自动保证线程安全,但是依然会降低效率,注意导包时导lang3的包
package com.doit.spark.day08
import java.text.SimpleDateFormat
import org.apache.commons.lang3.time.FastDateFormat
object DateUtils2 {
//val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
private val dateFormat: FastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss")
def dataToLong (dateU: String) = synchronized{
val time: Long = dateFormat.parse(dateU).getTime
time
}
}
解决方式四:
定义工具类时,使用class对象,然后在Driver端new工具类的对象,使用闭包的形式传入到函数中,如果对象中的信息量很大,就会在序列化后传输后,然后在多个Executor反序列化浪费大量的资源和时间
package com.doit.spark.day08
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
object DateDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("DateDemo")
val setMaster = args(0).toBoolean
if (setMaster){
conf.setMaster("local[*]")
}
val sc = new SparkContext(conf)
val textFile = args(1)
val line: RDD[String] = sc.textFile(textFile)
//new在Driver端
val dateUtils = new DateUtils
val maped: RDD[Long] = line.mapPartitions(it => {
it.map(x => {
val timeLong: Long = dateUtils.dataToLong(x)
timeLong
})
})
println(maped.collect().toBuffer)
}
}
解决方式五:
使用mapPartitions算子,然后在该算子内new工具类,这样即不会浪费大量的内存,也不会存在线程安全问题
package com.doit.spark.day08
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
object DateDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("DateDemo")
val setMaster = args(0).toBoolean
if (setMaster){
conf.setMaster("local[*]")
}
val sc = new SparkContext(conf)
val textFile = args(1)
val line: RDD[String] = sc.textFile(textFile)
//使用mapPartitions算子,每个分区new一个对象,该分区内的数据,共享该对象
//相当于一个Task用一个对象,所以不会存在线程安全问题,同时节省了内存空间
val maped: RDD[Long] = line.mapPartitions(it => {
//new在Executor端
val dateUtils = new DateUtils
it.map(x => {
val timeLong: Long = dateUtils.dataToLong(x)
timeLong
})
})
println(maped.collect().toBuffer)
}
}
案例:广播会变的ip规则数据
使用普通的广播变量广播之后,规则数据就不能再改变,所以,我们不能用广播变量的形式,将数据广播出去,我们可以使用object定义的单例对象,然后在该对象中定义一个ArrayBuffer储存规则数据,再在RDD函数中调用该对象,获取规则数据,在该函数被调用时(处理第一条数据时),就会初始化该对象,获取规则数据,然后所有的数据都可以使用该对象进行规则匹配
然后我们可以设置一个定时器,定时从某个文件中更新ArrayBuffer中的规则数据,更新后发送到RDD函数中,但需要加一个读写锁(ReentrantReadWriteLock),避免读写同时进行,出现线程安全问题.
代码实现
package com.doit.spark.day08
import java.io.{BufferedReader, FileInputStream, InputStreamReader}
import scala.collection.mutable.ArrayBuffer
object IpRulesLoader {
//定义一个可变数组ArrayBuffer,用来装规则数据
//在object中定义的定义的数据是静态的,在一个JVM进程中,只有一份
var ipRules = new ArrayBuffer[(Long, Long, String, String)]()
//加载IP规则数据,在Executor的类加载是执行一次
//静态代码块
//读取HDFS中的数据
//val fileSystem = FileSystem.get(URI.create("file://"), new Configuration())
//val inputStream = fileSystem.open(new Path("/Users/xing/Desktop/ip.txt"))
//读取本地的数据
private val bf = new BufferedReader(new InputStreamReader(new FileInputStream("文件路径")))
var line:String = null
//此处必须使用do while循环,因为它要循环的将文件中所有数据都读取到数组中,且必须至少先读取一次
do {
line = bf.readLine()
if (line != null){
//处理IP规则数据
val fields = line.split("[|]")
val startNum = fields(2).toLong
val endNum = fields(3).toLong
val province = fields(6)
val city = fields(7)
//放入元组,然后放入数组中
val t = (startNum, endNum, province, city)
ipRules += t
}
}while(line != null)
//调用该方法时.返回规则数组
def getAllRules(): ArrayBuffer[(Long, Long, String, String)] = {
ipRules
}
}
package com.doit.spark.day08
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable.ArrayBuffer
object IpLocationV1 {
def main(args: Array[String]): Unit = {
val isLocal = args(0).toBoolean
val conf = new SparkConf().setAppName(this.getClass.getSimpleName)
if(isLocal) {
conf.setMaster("local[*]")
}
val sc = new SparkContext(conf)
//指定未来读取日志数据的位置
val lines = sc.textFile(args(1))
val value: RDD[String] = lines.map(line => {
val fields = line.split("[|]")
val ip = fields(1)
//将IP地址转成10进制
val ipNumber: Long = IpUtils.ip2Long(ip)
//根据ipNumber查找规则,返回省市
val rules: ArrayBuffer[(Long, Long, String, String)] = IpRulesLoader.ipRules
val index: Int = IpUtils.binarySearch(rules, ipNumber)
var province = "未知"
if (index != -1) {
province = rules(index)._3
}
province
})
println(value.collect().toBuffer)
}
}
package com.doit.spark.day08
import scala.collection.mutable.ArrayBuffer
object IpUtils {
/**
* 将IP地址转成十进制
*
* @param ip
* @return
*/
def ip2Long(ip: String): Long = {
val fragments = ip.split("[.]")
var ipNum = 0L
for (i <- 0 until fragments.length) {
ipNum = fragments(i).toLong | ipNum << 8L
}
ipNum
}
/**
* 二分法查找
*
* @param lines
* @param ip
* @return
*/
def binarySearch(lines: ArrayBuffer[(Long, Long, String, String)], ip: Long): Int = {
var low = 0 //起始
var high = lines.length - 1 //结束
while (low <= high) {
val middle = (low + high) / 2
if ((ip >= lines(middle)._1) && (ip <= lines(middle)._2))
return middle
if (ip < lines(middle)._1)
high = middle - 1
else {
low = middle + 1
}
}
-1 //没有找到
}
def binarySearch(lines: Array[(Long, Long, String, String)], ip: Long): Int = {
var low = 0 //起始
var high = lines.length - 1 //结束
while (low <= high) {
val middle = (low + high) / 2
if ((ip >= lines(middle)._1) && (ip <= lines(middle)._2))
return middle
if (ip < lines(middle)._1)
high = middle - 1
else {
low = middle + 1
}
}
-1 //没有找到
}
}